BackendV2Client
Synapse Backend API v2 (/v2/*) 용 sync + async 클라이언트입니다.
v2 클라이언트는 기존 v1 클라이언트(BackendClient)와 나란히 존재하며 opt-in
방식입니다. 한 프로세스 안에서 둘을 함께 사용할 수 있습니다. v2 는 v1 과
세 가지 핵심 지점에서 다릅니다.
- Cursor 페이지네이션 —
{next, previous, results}(count 필드 없음). - Auth 헤더 포맷 —
SYNAPSE-ACCESS-TOKEN: syn_<token>와 rawSYNAPSE-Tenant: <code>(기본값에서Tokenprefix 미사용). - 리소스 속성 접근 —
client.list_projects(...)가 아닌client.projects.list(...)형식.
Synapse Backend 릴리즈
v2026.1.6(API contract2.0.0) 을 타겟합니다. 백엔드가 새 호환 릴리즈를 배포하면make sync-v2-all로 스키마를 재동기화 하고synapse_sdk.clients.backend_v2.SYNAPSE_BACKEND_VERSION상수를 업데이트하세요.
Overview
v2 백엔드는 22 개 도메인에 걸쳐 102 paths / 149 operations 를 노출합니다. SDK 는 현재 23 개 도메인 을 구현합니다 — 주요 CLI 워크플로우에 필요한 P0 15 개 + Phase 3 에서 추가된 P1 8 개:
| Group | Resources |
|---|---|
| Auth / infra | auth, tenants, tokens, schemas |
| Data | data_collections (+ nested groups), data_units, data_files |
| ML | ground_truth_datasets (+ nested versions), ground_truths, models, experiments |
| Workflow | projects (+ nested tags), tasks, assignments, reviews |
| Workspace (P1) | members, groups (top-level — data-collection groups 와 다름), validation_scripts, workshops |
| Execution (P1) | jobs, job_logs, plugins, plugin_releases |
전체 엔드포인트 카탈로그와 v1↔v2 diff 표 (v1 ↔ v2 endpoint diff
참조) 는
INVENTORY.md
에 있습니다.
Constructor
BackendV2Client(
base_url: str,
*,
access_token: str | None = None,
drf_token: str | None = None,
tenant: str | None = None,
tenant_token_prefix: bool = False,
timeout: dict[str, int] | None = None,
allow_insecure: bool = False,
)
Parameters
| Parameter | Type | Required | Default | 설명 |
|---|---|---|---|---|
base_url | str | Yes | – | 백엔드 호스트. /v2 prefix 는 포함하지 마세요 — 리소스가 자동으로 추가합니다. |
access_token | str | None | No | None | syn_* access token. SYNAPSE_BACKEND_V2_ACCESS_TOKEN 환경변수로 폴백. |
drf_token | str | None | No | None | /users/login/ 의 DRF token. SYNAPSE_BACKEND_V2_DRF_TOKEN 환경변수로 폴백. |
tenant | str | None | No | None | 테넌트 코드. SYNAPSE_BACKEND_V2_TENANT 환경변수로 폴백. |
tenant_token_prefix | bool | No | False | True 일 때 v1 호환을 위해 SYNAPSE-Tenant: Token <code> 형식으로 전송. |
timeout | dict[str, int] | None | No | {'connect': 5, 'read': 15} | Connect / read timeout (초). |
allow_insecure | bool | No | False | base_url 이 평문 HTTP 일 때 경고를 억제. |
AsyncBackendV2Client 는 동일한 인자를 받습니다. 단, timeout 은 dict 대신
httpx.Timeout 인스턴스를 받습니다.
Quick start
Synchronous
from synapse_sdk.clients.backend_v2 import BackendV2Client
client = BackendV2Client(
'https://api.test.synapse.sh',
access_token='syn_xxxxx',
tenant='acme',
)
# Cursor 페이지네이션 — 단일 페이지
page = client.projects.list(category='image', per_page=50)
for project in page.results:
print(project.id, project.title)
if page.next:
next_page = client.projects.list(cursor=page.next.split('cursor=')[-1])
# 모든 페이지 lazy 스트리밍
for project in client.projects.list(category='image', per_page=50, list_all=True):
print(project.id, project.title)
Asynchronous
import asyncio
from synapse_sdk.clients.backend_v2 import AsyncBackendV2Client
async def main() -> None:
async with AsyncBackendV2Client(
'https://api.test.synapse.sh',
access_token='syn_xxxxx',
tenant='acme',
) as client:
page = await client.projects.list(per_page=50).first_page()
async for project in client.projects.list(per_page=50):
print(project.id, project.title)
asyncio.run(main())
Authentication
v2 백엔드는 네 가지 인증 스킴을 지원합니다. 클라이언트는 전달된 인자/환경 변수에 따라 헤더를 조합합니다.
| Header | Value | Source |
|---|---|---|
SYNAPSE-ACCESS-TOKEN | syn_<token> | access_token= 인자 또는 SYNAPSE_BACKEND_V2_ACCESS_TOKEN env |
Authorization | Token <drf> | drf_token= 인자, SYNAPSE_BACKEND_V2_DRF_TOKEN env, 또는 client.auth.login() 반환값 |
SYNAPSE-Tenant | <code> (기본) 또는 Token <code> (legacy) | tenant= 인자 또는 SYNAPSE_BACKEND_V2_TENANT env. tenant_token_prefix=True 시 legacy prefix 적용 |
tenant_token_prefix 기본값 — v2-nativeapi.test.synapse.sh 에서 OQ-1 라이브 검증을 마친 이후
(Open items pending live verification
참조), tenant_token_prefix 의 기본값은 False 입니다 — SDK 가 raw 테넌트
코드(SYNAPSE-Tenant: <code>) 를 전송합니다. dev 백엔드는 양쪽 포맷을
모두 수용하므로, 아직 Token <code> prefix 를 기대하는 v1 스타일 배포는
tenant_token_prefix=True 로 명시적으로 opt-in 할 수 있습니다.
client = BackendV2Client(
'https://api.example.com',
access_token='syn_...',
tenant='acme',
tenant_token_prefix=True, # ⇒ "SYNAPSE-Tenant: Token acme"
)
mask_token() 헬퍼는 시크릿을 로그-안전 형식으로 변환합니다.
from synapse_sdk.clients.backend_v2 import mask_token
mask_token('syn_12345678901234') # 'syn_***1234'
mask_token(None) # '<unset>'
Login → access-token bootstrap
client = BackendV2Client('https://api.test.synapse.sh', tenant='acme')
# 1. 자격증명을 DRF token 으로 교환 (클라이언트에 자동 저장).
client.auth.login(email='[email protected]', password='...')
# 2. 장기간 유효한 access token 발급.
created = client.tokens.create({'description': 'sdk-cli'})
print(created.token) # 'syn_xxxxx' — 생성 시점에만 평문 반환
# 3. 클라이언트를 새 토큰으로 전환.
client.set_access_token(created.token)
client.set_drf_token(None)
Pagination
v2 백엔드는 DRF CursorPagination 을 사용합니다. transport 가
{data, meta} envelope 을 unwrap 한 뒤
(Response envelope handling 참조), 리소스
모듈은 다음 CursorPage 형태를 받습니다.
{
"next": "<cursor-or-null>",
"previous": "<cursor-or-null>",
"results": [...]
}
count 필드는 없습니다. 클라이언트는 페이지네이션을 다음과 같이 노출합니다.
| 호출 형태 | 반환 |
|---|---|
client.projects.list(per_page=50) | CursorPage[ProjectV2List] (단일 페이지) |
client.projects.list(per_page=50, list_all=True) | Iterator[ProjectV2List] (모든 페이지 스트리밍) |
await client.projects.list(per_page=50).first_page() (async) | CursorPage[ProjectV2List] |
async for project in client.projects.list(per_page=50) (async) | 모든 페이지 스트리밍 |
총 개수가 필요한 경우 사용자 코드에서 카운터를 직접 구현해야 합니다.
Response envelope handling
v2 백엔드는 모든 성공 응답을 {"data": ..., "meta": ...} envelope 으로
래핑합니다. transport (BackendV2Client._request /
AsyncBackendV2Client._request) 는 모든 성공 페이로드에 대해
unwrap_envelope
를 호출하여 리소스 모듈이 legacy 형태를 그대로 소비할 수 있게 합니다.
| Backend payload | Unwrap 후 |
|---|---|
{"data": [...], "meta": {"pagination": {"next_cursor": "...", "previous_cursor": null, "per_page": 50}}} | {"results": [...], "next": "...", "previous": null} |
{"data": {"id": 1, ...}, "meta": {"request_id": "..."}} | {"id": 1, ...} |
{"data": null, "meta": {...}} | null |
루트에 data 또는 meta 중 하나라도 누락된 페이로드 | 변경 없이 통과 (forward-compat passthrough) |
unwrap 은 엄격하게 조건부 입니다 — 루트에 data 와 meta 가 모두 존재할
때만 트리거됩니다. v1 스타일 페이로드, 픽스처, envelope 미적용 프록시는
그대로 통과합니다.
meta.request_id 는 unwrap 이 meta 를 폐기하기 전에 per-context 슬롯에
캡처되며,
client.last_request_id 와
client.capture_request_ids() 로
로그 상관관계 추적이 가능합니다.
Validation & Models
각 리소스는 OpenAPI 스키마 이름을 따른 Pydantic 모델을 반환합니다.
| Suffix | 용도 |
|---|---|
*V2List | List 응답 (slim payload) |
*V2Detail | Retrieve / create / update 응답 |
*V2CreateRequest | POST body |
Patched*V2CreateRequest | PATCH body |
response_model= / request_model= kwargs
transport 헬퍼 (client._get, client._post, client._put,
client._patch, client._delete 와 async 대응 메서드) 는 두 개의 선택적
Pydantic kwarg 를 받습니다. 리소스 모듈이 Model.model_validate(...) 를
직접 호출하는 대신 이 kwarg 에 의존할 수 있습니다.
# Before — 메서드마다 수동 검증
return ProjectV2Detail.model_validate(self._get(f'/v2/projects/{pid}/'))
# After — kwarg 기반 (리소스 메서드가 한 줄로 축약됨)
return self._get(f'/v2/projects/{pid}/', response_model=ProjectV2Detail)
# POST 도 Pydantic 인스턴스를 직접 받을 수 있음
return self._post(
'/v2/projects/',
data=ProjectV2CreateRequest(title='demo'), # sync 는 `data=`
request_model=ProjectV2CreateRequest,
response_model=ProjectV2Detail,
)
# Async 는 `data=` 대신 `json=` 을 사용합니다.
동작:
response_model=Model— raw dict 가 아닌 완성된Model인스턴스를 반환.request_model=Model— Pydantic 인스턴스 또는 dict 를 받으며, 둘 다model_dump(by_alias=True, exclude_none=True, mode='json')으로 직렬화됩니다. 이는 기존resources/_helpers.model_dump헬퍼와 동일한 wire 규약입니다. caller 가 Pydantic 인스턴스를 보내든 dict 를 보내든 wire shape 은 동일합니다.- 응답 디코딩 중 발생한
pydantic.ValidationError는synapse_sdk.exceptions.ValidationError로 wrap 됩니다. 원본 예외는__cause__로 보존됩니다. - 요청 검증 중 발생한
pydantic.ValidationError는 그대로 전파됩니다 — caller 가 제공한 데이터는 디버깅을 위해 pydantic 의 풍부한 에러 정보가 유용하기 때문입니다 (의도적 비대칭). - 두 kwarg 모두 생략하면 transport 동작은 변경되지 않습니다.
SYN-6854 번들에서 read 와 write 도메인의 60+ 리소스 메서드 전부가
response_model= / request_model= kwarg 패턴으로 전환되었습니다. 호출자
시그니처와 wire shape 은 변하지 않았습니다 — 순수 내부 변경이지만, 이제
pydantic.ValidationError 가 SDK 호출 경로에서 누설되지 않습니다.
synapse_sdk.exceptions.ValidationError 를 catch 하고 raw
pydantic detail 이 필요하면 __cause__ 를 확인하세요.
ValidationError.detail 로 누설되지 않습니다mixin 은 wrap 전에 pydantic.ValidationError.errors(include_input=False) 를
호출하여 각 에러 엔트리에서 raw 응답 본문을 제거합니다. 응답에는 시크릿
(예: tokens.create() 가 반환하는 평문 syn_* token) 이 포함될 수 있고,
SDK 사용자는 종종 ValidationError.detail 을 외부 로깅 sink 로 전달합니다.
완전한 진단 정보는 exc.__cause__ (원본 pydantic.ValidationError) 를
통해 그대로 접근할 수 있습니다.
from synapse_sdk.exceptions import ValidationError as SdkValidationError
import pydantic
try:
project = client.projects.retrieve(42)
except SdkValidationError as exc:
# 단일 SDK 고유 예외 타입 — bare pydantic 은 절대 노출되지 않음
print(exc.detail['model']) # 'ProjectV2Detail'
print(exc.detail['detail']) # 에러 dict 리스트 ('input' 키 없음)
if isinstance(exc.__cause__, pydantic.ValidationError):
# 진단용 전체 pydantic detail (raw input 포함).
for err in exc.__cause__.errors():
print(err['loc'], err['msg'])
kwarg 경로는
BackendV2ValidationMixin
에 구현되어 있고 sync 와 async 클라이언트가 MRO 를 통해 공유합니다.
Error handling
BackendV2Client 는 v1 클라이언트와 동일한 표준 SDK 예외 계층 (synapse_sdk.exceptions) 을
raise 합니다.
from synapse_sdk.exceptions import (
AuthenticationError,
AuthorizationError,
NotFoundError,
RateLimitError,
ServerError,
ValidationError,
)
try:
project = client.projects.retrieve(42)
except NotFoundError:
...
except AuthenticationError:
# 재로그인 또는 access token 갱신.
...
except ValidationError as e:
# 400 / 422 — 백엔드 `detail` 페이로드가 예외에 보존됨.
# `response_model=` 디코딩 실패 시에도 raise — Validation & Models 참조.
...
Resource map
동일한 표면적이 BackendV2Client (sync) 와 AsyncBackendV2Client (async)
양쪽에서 제공됩니다.
Top-level resources
| Attribute | Endpoints | Notable methods |
|---|---|---|
client.auth | POST /users/login/ | login(email, password, set_drf_token=True) |
client.tenants | GET /v2/tenants/(...) | list, retrieve(code) |
client.tokens | */v2/tokens/(...) | list, create, retrieve(id), destroy(id) |
client.schemas | GET /v2/schemas/... | annotation_configurations(), file_specifications() |
client.data_collections | */v2/data-collections/(...) | full CRUD + membership + nested groups(dc_id) |
client.data_units | */v2/data-units/(...) | full CRUD + permission trio |
client.data_files | GET /v2/data-files/(...) | list, retrieve(id) |
client.ground_truth_datasets | */v2/ground-truth-datasets/(...) | full CRUD + nested versions(dataset_id) |
client.ground_truths | GET /v2/ground-truths/(...) | list, retrieve(id) |
client.models | GET /v2/models/(...) | list, retrieve(id) |
client.experiments | */v2/experiments/(...) | full CRUD + membership family |
client.projects | */v2/projects/(...) | full CRUD + membership + nested tags(project_id) |
client.tasks | */v2/tasks/(...) | full CRUD + permission trio |
client.assignments | GET /v2/assignments/(...) | list, retrieve(id) + permission trio |
client.reviews | */v2/reviews/(...) | list, create, retrieve(id) + permission trio |
client.members | */v2/members/(...) | list, retrieve(id), create |
client.groups | */v2/groups/(...) | full CRUD (top-level workspace groups) |
client.validation_scripts | */v2/validation-scripts/(...) | full CRUD |
client.workshops | */v2/workshops/(...) | list, retrieve(id) + permission trio |
client.jobs | GET /v2/jobs/(...) | list, retrieve(id) (UUID-keyed) |
client.job_logs | GET /v2/job-logs/(...) | list, retrieve(id) |
client.plugins | GET /v2/plugins/(...) | list, retrieve(id) |
client.plugin_releases | GET /v2/plugin-releases/(...) | list, retrieve(id) |
Membership family
Top-level 리소스 (projects, experiments, data_collections) 는 다음을
노출합니다.
.check_permission(id, permission='view')
.permissions(id)
.roles(id)
.grantable_roles(id)
.default_roles()
.invite(id, payload)
.get_join_requests(id)
.request_join(id, payload)
.confirm_join_request(id, payload)
.my_join_requests()
.my_join_request_detail(join_request_id)
Permission trio
자식 / leaf 리소스 (tasks, data_units, assignments, reviews,
workshops, nested groups (data-collections 하위), nested versions,
nested tags) 는 다음만 노출합니다.
.check_permission(id, permission='view')
.permissions(id)
.roles(id)
Membership helpers — typed payloads
Top-level 리소스 (projects, experiments, data_collections) 의
membership 메서드는 mutating verb 에 대해 타입이 지정된 payload 모델 을
받습니다.
from synapse_sdk.clients.backend_v2.models import (
MemberRoleJoinRequestCreateRequest,
PatchedMemberRoleJoinRequestConfirmRequest,
TargetRoleCreateRequest,
)
# 1) 대상 사용자/그룹에 role 을 부여하면서 invite.
client.projects.invite(
project_id,
TargetRoleCreateRequest(target=42, role=7),
)
# 2) 기존 프로젝트 가입 요청.
client.projects.request_join(
project_id,
MemberRoleJoinRequestCreateRequest(role=7, message='please add me'),
)
# 3) 가입 요청 confirm / reject.
client.projects.confirm_join_request(
project_id,
PatchedMemberRoleJoinRequestConfirmRequest(confirm=True),
)
dict 를 그대로 전달해도 됩니다 — wire shape 은 동일합니다.
helpers 자체는
resources/_membership.py
에 두 @runtime_checkable Protocol (SyncMembershipResource /
AsyncMembershipResource) 뒤에 위치합니다. 정적 타입 체커 (mypy, pyright)
와 런타임 가드가 sync↔async cross-wiring 을 거부하므로
coroutine was never awaited 가 코드 리뷰를 슬쩍 통과하지 못합니다.
P1 도메인 quick examples
# Workspace members (top-level /v2/members/)
page = client.members.list(per_page=20)
member = client.members.retrieve(123)
# Workspace groups (top-level — data-collection groups 와 다름)
group = client.groups.create({'name': 'reviewers'})
client.groups.partial_update(group.id, {'name': 'qa-reviewers'})
# Validation scripts
script = client.validation_scripts.retrieve(7)
# Workshops (read-only + permission trio)
ws = client.workshops.retrieve(3)
client.workshops.check_permission(3, permission='view')
# Async jobs (UUID-keyed) 와 console logs
job = client.jobs.retrieve('00000000-0000-0000-0000-000000000001')
for log in client.job_logs.list(job=job.id, list_all=True):
print(log.message)
# Plugins + releases (read-only)
client.plugins.list(per_page=10)
client.plugin_releases.retrieve(42)
Common patterns
자동 생성 모델로 요청 본문 만들기
Pydantic 모델은 Validation & Models 에 정리되어 있습니다. 빠른 예시:
from synapse_sdk.clients.backend_v2.models import ProjectV2CreateRequest
payload = ProjectV2CreateRequest(
title='New project',
category='image',
data_collection=10,
)
project = client.projects.create(payload)
dict 를 그대로 전달해도 됩니다 — 백엔드가 검증합니다.
환경변수로 부트스트랩
import os
os.environ['SYNAPSE_BACKEND_V2_ACCESS_TOKEN'] = 'syn_...'
os.environ['SYNAPSE_BACKEND_V2_TENANT'] = 'acme'
# 세 값 모두 생성자에서 생략 가능.
client = BackendV2Client('https://api.test.synapse.sh')
중첩 리소스 사용
# Data collection groups
client.data_collections.groups(dc_id=7).list(per_page=20)
client.data_collections.groups(dc_id=7).create({'name': 'g1'})
# Ground truth dataset versions
client.ground_truth_datasets.versions(dataset_id=3).list()
# Project tags
client.projects.tags(project_id=42).list(per_page=10)
client.projects.tags(project_id=42).create({'name': 'urgent', 'category': 'project'})
Observability — last_request_id trace
성공한 모든 v2 응답에는 백엔드가 해당 요청에 대해 로깅하는
meta.request_id 가 포함됩니다. transport 는
unwrap_envelope 가 meta 를 폐기하기
전에, 이를
contextvars.ContextVar
(설계상 async-safe / thread-safe) 에 캡처합니다. 두 가지 표면이 노출됩니다.
| Surface | 반환 | 사용처 |
|---|---|---|
client.last_request_id (property) | str | None | 현재 컨텍스트에서 관측된 가장 최근 request_id 를 읽기. |
client.capture_request_ids() (context manager) | None | 진입 시 슬롯을 reset, 종료 시 외부 값을 복원 — 작업 블록을 자체 request_id 범위로 한정. |
project = client.projects.retrieve(42)
print(client.last_request_id) # 예: 'req_01H8...'
with client.capture_request_ids():
page = client.projects.list(per_page=20)
log.info('listed', request_id=client.last_request_id)
# 외부 컨텍스트의 request_id 가 여기서 복원됩니다.
async 클라이언트도 같은 표면을 제공합니다 — last_request_id 와
capture_request_ids() 모두 AsyncBackendV2Client 에서 사용 가능합니다.
ContextVar 가 asyncio.Task 컨텍스트 캡처에 참여하므로
asyncio.gather(call_a(), call_b()) 는 race 가 발생하지 않습니다 — 각
태스크가 자체 last_request_id 를 봅니다.
구현체:
observability.py.
record_request_id 는 unwrap_envelope 와 동일한 게이트를 사용합니다 (루트
에 data 와 meta 가 모두 있을 때만 트리거) — envelope 미적용 응답은 슬롯
을 건드리지 않으므로 이전 request_id 값이 보존됩니다.
CLI — synapse v2 ...
SDK 는 BackendV2Client 를 감싸는 read-only CLI sub-command 를 제공합니다 —
즉석 작업과 테넌트 smoke 테스트용입니다. 인증은 기존 synapse login 플로우
(~/.synapse/config.json) 와 공유됩니다.
synapse v2 projects list --tenant acme --per-page 20
synapse v2 projects retrieve 42 --tenant acme
synapse v2 tokens list --tenant acme
synapse v2 tokens retrieve 7 --tenant acme
synapse v2 tenants list
synapse v2 tenants retrieve 1
| Flag / env | 용도 |
|---|---|
--tenant | 테넌트 코드. SYNAPSE_BACKEND_V2_TENANT 환경변수로 폴백. |
--host | API 호스트. synapse login 의 호스트가 기본값. |
--token / -t | Access token. synapse login 의 토큰이 기본값. |
--drf-token | Legacy DRF token. 보통 생략. |
--per-page | List 페이지 크기 (기본 20). |
list 명령은 Rich 테이블을 출력하고, projects list 는 추가로
client.last_request_id 를 echo 하므로 백엔드 로그 상관 추적이 셸 명령
한 번에 끝납니다. mutating verb (create / update / destroy) 는
follow-up PR 로 보류됐습니다 — 더 풍부한 입력 (JSON body, confirm prompt,
이중 인증) 이 필요해 smoke MVP 의 표면을 부풀립니다.
CLI 는
synapse_sdk/cli/v2.py
에 위치하며 synapse_sdk.cli.main 에서 cli.add_typer(v2_app, name='v2') 로
등록됩니다.
Performance — lazy imports
BackendV2Client 는 import 비용이 가볍게 설계됐습니다. 두 레이어가
참여합니다.
- 리소스 속성 — lazy
@property접근자입니다.client.projects는 첫 사용 시점에만 리소스 객체를 생성합니다. - Pydantic 모델 —
synapse_sdk.clients.backend_v2.models는 자동 생성된_generated모듈을 PEP 562__getattr__로 re-export 합니다. 1900 줄짜리_generated모듈 (137 개 pydantic 클래스) 은 import 시점이 아닌 첫 attribute 접근 시점에 로드됩니다.
# Import-time 작업: 최소 — _generated load 없음.
from synapse_sdk.clients.backend_v2 import BackendV2Client
# _generated import 트리거 (1회, cold ~140ms).
from synapse_sdk.clients.backend_v2.models import ProjectV2List
BackendV2Client 만 사용하고 모델 클래스를 직접 import 하지 않는 워크플로
우는 ~140ms cold-import 비용을 절감합니다. 호출자 관점에서 lazy export 는
투명합니다 — from synapse_sdk.clients.backend_v2.models import ProjectV2List
는 이전과 정확히 동일하게 동작합니다.
Optional dependencies
email-validator 는 Phase 4 에서 런타임 의존성에서 제거 되었습니다.
유일한 EmailStr 사용처 (UserThumbnailInfo.email) 는 read-only 응답
필드입니다 — 백엔드 검증이 권위적이므로 클라이언트 측 RFC 5322 강제는
가치가 없습니다. codegen 후 패치 (scripts/sync_v2_schema.py 의
_patch_email_str) 가 생성된 모든 EmailStr 어노테이션을 str 로 재작성
하면서 max_length=254 제약은 보존하므로, 모델은 email-validator 런타임
의존성 없이도 문서화된 길이를 계속 검증합니다.
UserThumbnailInfo 를 적대적 입력으로 직접 구성하면서 RFC 5322 검증이
필요한 워크플로우라면, email-validator 를 직접 설치해 값을 사전 검증
하세요. 대부분의 호출자는 이 작업이 필요하지 않습니다.
v1 ↔ v2 endpoint diff
INVENTORY.md 의 "Diff vs v1" 섹션은 make sync-v2-all (또는 단독 타겟
make gen-v2-diff) 가 자동 생성합니다. v1 경로는
synapse_sdk/clients/backend/*.py 에서 introspect 하고 v2 스키마와 비교
합니다 (매칭 전에 /v2/ prefix 제거).
| Bucket | Count | 의미 |
|---|---|---|
| v1 only | 31 | BackendClient 만 노출하는 엔드포인트 — v2 대응이 없음. v1 클라이언트를 계속 사용하세요 (agents, storages, legacy serve_applications/ 등). |
| v2 only | 101 | v2 고유의 신규 표면 — membership / permission trio 라우트, workshops 등. |
| Shared | 5 | 양쪽에서 동일한 method+path (예: GET /jobs/, POST /tasks/). 편한 시점에 마이그레이션. |
전체 표는
INVENTORY.md
에 있습니다. 백엔드가 새 릴리즈를 배포할 때마다 make sync-v2-all 을
재실행하면 표가 in-place 로 업데이트됩니다.
Migration from v1 (BackendClient)
| 항목 | v1 | v2 |
|---|---|---|
| Pagination | count/next/previous/results (offset) | next/previous/results (cursor; count 없음) |
| 페이지 크기 파라미터 | page_size= | per_page= |
| Tenant 헤더 | Synapse-Tenant: Token <code> | SYNAPSE-Tenant: <code> (기본; tenant_token_prefix=True 로 opt-back) |
| Access token 헤더 | Synapse-Access-Token: Token <token> | SYNAPSE-ACCESS-TOKEN: syn_<token> |
| 메서드 형태 | flat (client.list_projects(...)) | namespaced (client.projects.list(...)) |
| 응답 | raw dict | typed Pydantic model (*V2List / *V2Detail) |
두 클라이언트는 공존 가능합니다. v2 엔드포인트가 사용 워크로드에 가용해질 때마다 호출 지점을 점진적으로 전환하세요.
Schema sync & drift handling
Pydantic 모델과 INVENTORY.md 는 캐시된 OpenAPI 스키마
(schema/openapi.yaml) 에서 파생됩니다. 백엔드가 새 릴리즈를 배포하면 다음
Make 타겟으로 갱신하세요.
make sync-v2-schema # 스키마만 다운로드 + 캐시
make gen-v2-models # Pydantic 모델 재생성
make gen-v2-inventory # INVENTORY.md 재생성
make sync-v2-all # 전부 실행
스크립트 위치:
scripts/sync_v2_schema.py.
기본적으로 api.test.synapse.sh 에서 다운로드합니다 — --host 로 override
가능.
라이브 백엔드와 OpenAPI 의 drift (Path γ)
dev 백엔드가 OpenAPI 스키마에 인코딩되어 있지 않거나 잘못 인코딩된 형태를 때때로 반환하는 경우가 있습니다. 이 때 생성된 모델을 직접 손대는 대신, SDK 는 codegen + override 하이브리드 ("Path γ") 를 사용합니다.
make gen-v2-models가 캐시된 스키마를 단일 출처로 삼아datamodel-codegen을 실행합니다.scripts/sync_v2_schema.py가 생성된 트리에 선언적 build-time override 를 적용합니다.- 각 override 는 자가 진단됩니다 — 상위 스키마가 보정되면 override 가
SystemExit를 raise 하여 contributor 가 drift 가 해소되었음을 인지하고 override 를 제거할 수 있도록 합니다.
현재 트래킹 중인 override:
| Drift | 위치 | Override |
|---|---|---|
Category10 enum 의 data 멤버 누락 (라이브 백엔드는 category=data 를 반환하지만 스키마가 누락) | _patch_enum_drift 의 _ENUM_DRIFT = {'Category10': ['data']} | codegen 후 누락된 enum 멤버 추가 |
JobV2Detail.completed / JobV2List.completed 가 nullable+required 로 선언되었으나 스키마 본문에 누락 | 동일 스크립트의 모델 patcher | 속성을 Optional[...] 로 격상하여 라이브 null 페이로드 검증 통과 |
스크립트 헤더에 상위 이슈를 문서화하고, 자가 진단 가드 (SystemExit 스타일
assertion) 를 포함하여 스키마가 수정되는 즉시 override 가 코드베이스에서
스스로 제거되도록 하세요. 문서화되지 않은 장기 override 는 안티패턴입니다.
Open items pending live verification
다음 항목들은 자격증명이 필요한 dev 백엔드 액세스를 요구합니다. 클라이언트는 안전한 기본값으로 출하되지만, 테넌트 환경이 다르다면 연결된 노브를 조정하세요.
- OQ-1 (해결됨 — SYN-6854) —
SYNAPSE-Tenant값 형식. dev 백엔드는 raw 테넌트 코드와Token <code>를 모두 수용합니다. SDK 기본값은 raw 값 (tenant_token_prefix=False) 입니다. v1 스타일 배포는tenant_token_prefix=True로 opt-back 가능합니다. - OQ-2 (보류 — 라이브 데이터 필요) —
per_page상한. dev 백엔드가 1 ~ 10 000 모든 페이지 크기에 대해 빈 결과 셋과 함께 200 을 반환하여 실제 cap 을 아직 관측할 수 없습니다. SDK 는 cap 을 강제하지 않습니다 —specs/backend-v2-followup-roadmap/에서 트래킹. - OQ-3 (해결됨 — SYN-6854) —
tokens.create()평문 token 노출. dev 백엔드가 create 응답에 평문syn_*token 을 포함하여 반환합니다 (AccessTokenCreateResponse.token: SecretStr | None).