Skip to content

Commit 56d5552

Browse files
authored
Merge pull request #4 from sudomock/eski-erisimci-eski-uc
0.11.1: eski erişimciler eski uca sabitlendi
2 parents eed5305 + 64d5f4a commit 56d5552

11 files changed

Lines changed: 805 additions & 69 deletions

‎CHANGELOG.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.11.1] - 2026-09-19
11+
12+
### Fixed
13+
- **`client.ai` and `client.mockups` call the endpoints they have always
14+
called.** In 0.11.0 both accessors were aliases of `client.photo_mockups` /
15+
`client.psd_mockups` and so began calling `/api/v1/photo-mockups` and
16+
`/api/v1/psd-mockups`. Code that upgraded the SDK without being rewritten
17+
therefore opened its jobs on the current path and received the current job
18+
kinds (`photo_mockup_create` / `photo_mockup_render`) where it had always
19+
received `2d_create` / `2d_render` — a branch on `kind` stopped matching
20+
without raising anything. `client.ai` is pinned back to
21+
`/api/v1/sudoai/2d-mockups` and `client.mockups` to `/api/v1/mockups`, the
22+
paths their callers were already using and that the API keeps serving, so
23+
upgrading to this release changes nothing for code written against the
24+
earlier names. `client.photo_mockups` and `client.psd_mockups` are unchanged
25+
and keep calling the current paths; move an accessor over when you are ready
26+
to read the current job kinds. The 0.11.0 note that an SDK had to stay pinned
27+
below 0.11.0 to keep the earlier paths no longer applies.
28+
- Assigning to `client.ai` or `client.mockups` reaches the earlier accessor, so
29+
a test double injected under either name is the object that gets called.
30+
- **Assigning to `client.ai` or `client.mockups` no longer moves
31+
`client.photo_mockups` / `client.psd_mockups`.** The setter wrote both
32+
accessors while the getter read only the earlier one, so the ordinary
33+
save-and-restore pattern around a test double (`saved = client.ai` ...
34+
`client.ai = saved`, or `mock.patch.object`) left the current accessor
35+
pointing at the earlier endpoint for the rest of the process — every later
36+
`client.photo_mockups` call went to `/api/v1/sudoai/2d-mockups` and came back
37+
with the earlier job kinds, with nothing raised. Each earlier name now reads
38+
back exactly what was written to it and leaves the current name alone, so a
39+
restore puts both accessors where they started.
40+
- **`del client.ai` and `del client.mockups` work.** The earlier names had no
41+
deleter, so `mock.patch.object(client, "ai", double)` raised
42+
`AttributeError: property 'ai' of 'SudoMock' object has no deleter` when its
43+
block ended. Deleting an earlier name drops the override and restores the
44+
accessor the client was built with, still pinned to the endpoint that name
45+
has always called.
46+
47+
### Changed
48+
- `webhook_endpoints.create(...)` docs: an endpoint that does not pass
49+
`event_naming` is pinned by the spelling of its `events` — the earlier event
50+
names pin it to `"legacy"`, the current names to `"current"`, an empty or
51+
mixed list to `"legacy"`. The earlier note that the API always pinned a new
52+
endpoint to `"current"` no longer describes what happens. Passing
53+
`event_naming` explicitly decides it and is unaffected.
54+
55+
1056
## [0.11.0] - 2026-09-19
1157

1258
### Added

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,7 +543,7 @@ client = SudoMock(
543543
| Method | Description |
544544
|--------|-------------|
545545
| `client.webhook_endpoints.list()` | List registered endpoints |
546-
| `client.webhook_endpoints.create(url=, events=, description=None, event_naming=None)` | Register an endpoint (empty `events` = all; `event_naming` `"current"` / `"legacy"`, API default `"current"`) |
546+
| `client.webhook_endpoints.create(url=, events=, description=None, event_naming=None)` | Register an endpoint (empty `events` = all; `event_naming` `"current"` / `"legacy"`; left out, the API follows the spelling of `events`) |
547547
| `client.webhook_endpoints.get(uuid)` | Get an endpoint |
548548
| `client.webhook_endpoints.update(uuid, url=, events=, description=, enabled=, event_naming=)` | Update an endpoint (`event_naming` re-pins it to `"current"` or `"legacy"`) |
549549
| `client.webhook_endpoints.delete(uuid)` | Delete an endpoint |

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "sudomock"
7-
version = "0.11.0"
7+
version = "0.11.1"
88
description = "Official Python SDK for the SudoMock Mockup Generator API"
99
readme = "README.md"
1010
license = "MIT"

‎src/sudomock/_http.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
DEFAULT_RENDER_TIMEOUT = 120.0
4545
DEFAULT_MAX_RETRIES = 3
4646

47+
# Endpoints the current accessors (``photo_mockups`` / ``psd_mockups``) call.
48+
PHOTO_MOCKUPS_PATH = "/api/v1/photo-mockups"
49+
PSD_MOCKUPS_PATH = "/api/v1/psd-mockups"
50+
51+
# Endpoints the earlier accessors (``ai`` / ``mockups``) have always called and
52+
# keep calling. Code written against those names was never rewritten, so the
53+
# request it produces must not change either. Both remain served by the API.
54+
EARLIER_PHOTO_MOCKUPS_PATH = "/api/v1/sudoai/2d-mockups"
55+
EARLIER_PSD_MOCKUPS_PATH = "/api/v1/mockups"
56+
4757

4858
# ---------------------------------------------------------------------------
4959
# Error mapping

‎src/sudomock/async_client.py‎

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
DEFAULT_MAX_RETRIES,
4444
DEFAULT_RENDER_TIMEOUT,
4545
DEFAULT_TIMEOUT,
46+
EARLIER_PHOTO_MOCKUPS_PATH,
47+
EARLIER_PSD_MOCKUPS_PATH,
48+
PHOTO_MOCKUPS_PATH,
49+
PSD_MOCKUPS_PATH,
4650
AsyncTransport,
4751
)
4852
from ._public_contract import public_2d_render_targets
@@ -85,8 +89,9 @@
8589
class _AsyncPsdMockupsResource:
8690
"""Async PSD mockup template operations (list, get, update, delete)."""
8791

88-
def __init__(self, transport: AsyncTransport) -> None:
92+
def __init__(self, transport: AsyncTransport, base: str = PSD_MOCKUPS_PATH) -> None:
8993
self._transport = transport
94+
self._base = base
9095

9196
async def list(
9297
self,
@@ -116,7 +121,7 @@ async def list(
116121
"""
117122
resp = await self._transport.request(
118123
"GET",
119-
"/api/v1/psd-mockups",
124+
self._base,
120125
params={
121126
"limit": limit,
122127
"offset": offset,
@@ -142,7 +147,7 @@ async def get(self, uuid: str) -> Mockup:
142147
Raises:
143148
NotFoundError: If the mockup does not exist.
144149
"""
145-
resp = await self._transport.request("GET", f"/api/v1/psd-mockups/{uuid}")
150+
resp = await self._transport.request("GET", f"{self._base}/{uuid}")
146151
payload = resp.json()
147152
return Mockup.model_validate({**payload["data"], "warnings": payload.get("warnings") or []})
148153

@@ -159,9 +164,7 @@ async def update(self, uuid: str, *, name: str) -> Mockup:
159164
Raises:
160165
NotFoundError: If the mockup does not exist.
161166
"""
162-
resp = await self._transport.request(
163-
"PATCH", f"/api/v1/psd-mockups/{uuid}", json={"name": name}
164-
)
167+
resp = await self._transport.request("PATCH", f"{self._base}/{uuid}", json={"name": name})
165168
payload = resp.json()
166169
return Mockup.model_validate({**payload["data"], "warnings": payload.get("warnings") or []})
167170

@@ -174,7 +177,7 @@ async def delete(self, uuid: str) -> None:
174177
Raises:
175178
NotFoundError: If the mockup does not exist.
176179
"""
177-
await self._transport.request("DELETE", f"/api/v1/psd-mockups/{uuid}")
180+
await self._transport.request("DELETE", f"{self._base}/{uuid}")
178181

179182

180183
class _AsyncRendersResource:
@@ -481,9 +484,11 @@ async def create(
481484
event_naming: Which spelling of the photo-mockup events this endpoint
482485
receives: ``"current"`` (``photo_mockup.*``,
483486
``photo_mockup_render.*``) or ``"legacy"`` (``2d_mockup.*``,
484-
``2d_render.*``). Left out, the API pins a new endpoint to
485-
``"current"``; pass ``"legacy"`` for a handler that still reads
486-
the older names.
487+
``2d_render.*``). Left out, the API reads the spelling of
488+
``events``: a list written in the earlier names pins the
489+
endpoint to ``"legacy"``, a list written in the current names
490+
pins it to ``"current"``, and an empty or mixed list pins it to
491+
``"legacy"``. Pass the value outright to decide it yourself.
487492
"""
488493
# API field is `event_types` (empty list = subscribe to all events).
489494
# NOTE: the create endpoint has no `enabled` field (it is update-only).
@@ -601,8 +606,9 @@ async def replay_failed(self, uuid: str) -> None:
601606
class _AsyncPhotoMockupsResource:
602607
"""Async photo mockup operations (create, render, list, get, delete, print areas)."""
603608

604-
def __init__(self, transport: AsyncTransport) -> None:
609+
def __init__(self, transport: AsyncTransport, base: str = PHOTO_MOCKUPS_PATH) -> None:
605610
self._transport = transport
611+
self._base = base
606612

607613
async def create(
608614
self,
@@ -658,7 +664,7 @@ async def create(
658664

659665
resp = await self._transport.request(
660666
"POST",
661-
"/api/v1/photo-mockups",
667+
self._base,
662668
json=body,
663669
headers={"Idempotency-Key": idempotency_key},
664670
)
@@ -722,7 +728,7 @@ async def update_2d_print_areas(
722728
"""
723729
resp = await self._transport.request(
724730
"PUT",
725-
f"/api/v1/photo-mockups/{mockup_id}/print-areas",
731+
f"{self._base}/{mockup_id}/print-areas",
726732
json={"print_areas": print_areas},
727733
)
728734
return PhotoMockupPrintAreasUpdate.model_validate(resp.json()["data"])
@@ -793,7 +799,7 @@ async def render(
793799

794800
resp = await self._transport.request(
795801
"POST",
796-
f"/api/v1/photo-mockups/{mockup_uuid}/render",
802+
f"{self._base}/{mockup_uuid}/render",
797803
json=body,
798804
timeout=self._transport._render_timeout,
799805
)
@@ -814,7 +820,7 @@ async def list(
814820
"""List your photo mockups (free; zero credits)."""
815821
resp = await self._transport.request(
816822
"GET",
817-
"/api/v1/photo-mockups",
823+
self._base,
818824
params={
819825
"limit": limit,
820826
"offset": offset,
@@ -835,7 +841,7 @@ async def get(self, mockup_id: str) -> PhotoMockup:
835841
Raises:
836842
NotFoundError: If the photo mockup does not exist.
837843
"""
838-
resp = await self._transport.request("GET", f"/api/v1/photo-mockups/{mockup_id}")
844+
resp = await self._transport.request("GET", f"{self._base}/{mockup_id}")
839845
return PhotoMockup.model_validate(resp.json()["data"])
840846

841847
async def delete(self, mockup_id: str) -> None:
@@ -844,7 +850,7 @@ async def delete(self, mockup_id: str) -> None:
844850
Raises:
845851
NotFoundError: If the photo mockup does not exist.
846852
"""
847-
await self._transport.request("DELETE", f"/api/v1/photo-mockups/{mockup_id}")
853+
await self._transport.request("DELETE", f"{self._base}/{mockup_id}")
848854

849855

850856
class _AsyncImagesResource:
@@ -1077,35 +1083,92 @@ def __init__(
10771083
self.packages = _AsyncPackagesResource(self._transport)
10781084
self.webhook_endpoints = _AsyncWebhookEndpointsResource(self._transport)
10791085

1086+
# Same resources, pinned to the endpoints the earlier accessors have
1087+
# always called. Kept as their own objects so ``photo_mockups`` and
1088+
# ``psd_mockups`` can move on without moving the earlier callers.
1089+
self._earlier_photo_mockups = _AsyncPhotoMockupsResource(
1090+
self._transport, base=EARLIER_PHOTO_MOCKUPS_PATH
1091+
)
1092+
self._earlier_psd_mockups = _AsyncPsdMockupsResource(
1093+
self._transport, base=EARLIER_PSD_MOCKUPS_PATH
1094+
)
1095+
10801096
@property
10811097
def ai(self) -> _AsyncPhotoMockupsResource:
1082-
"""Earlier name of :attr:`photo_mockups`; emits a ``DeprecationWarning``."""
1098+
"""Earlier name of :attr:`photo_mockups`; emits a ``DeprecationWarning``.
1099+
1100+
Calls made through this name keep going to the endpoint they have
1101+
always gone to, so a job opened here is the job this caller has always
1102+
opened. Use :attr:`photo_mockups` for the current endpoint.
1103+
"""
10831104
warnings.warn(
10841105
"AsyncSudoMock.ai is deprecated; use AsyncSudoMock.photo_mockups instead.",
10851106
DeprecationWarning,
10861107
stacklevel=2,
10871108
)
1088-
return self.photo_mockups
1109+
return self._earlier_photo_mockups
10891110

10901111
@ai.setter
10911112
def ai(self, value: object) -> None:
1092-
"""Assignment still works so existing test doubles keep running."""
1093-
self.photo_mockups = value # type: ignore[assignment]
1113+
"""Assignment still works so existing test doubles keep running.
1114+
1115+
What is written here is what this name reads back, and only this name:
1116+
:attr:`photo_mockups` is left where it is. A caller that saves this
1117+
accessor, swaps in a double and writes the saved value back therefore
1118+
puts both accessors exactly where they started.
1119+
"""
1120+
self._earlier_photo_mockups = value # type: ignore[assignment]
1121+
1122+
@ai.deleter
1123+
def ai(self) -> None:
1124+
"""``del client.ai`` drops the override and restores the default.
1125+
1126+
The accessor goes back to the one the client was built with, still
1127+
pinned to the endpoint this name has always called.
1128+
``mock.patch.object`` deletes the attribute when its block ends, so
1129+
this is the way back from a patched double.
1130+
"""
1131+
self._earlier_photo_mockups = _AsyncPhotoMockupsResource(
1132+
self._transport, base=EARLIER_PHOTO_MOCKUPS_PATH
1133+
)
10941134

10951135
@property
10961136
def mockups(self) -> _AsyncPsdMockupsResource:
1097-
"""Earlier name of :attr:`psd_mockups`; emits a ``DeprecationWarning``."""
1137+
"""Earlier name of :attr:`psd_mockups`; emits a ``DeprecationWarning``.
1138+
1139+
Calls made through this name keep going to the endpoint they have
1140+
always gone to. Use :attr:`psd_mockups` for the current endpoint.
1141+
"""
10981142
warnings.warn(
10991143
"AsyncSudoMock.mockups is deprecated; use AsyncSudoMock.psd_mockups instead.",
11001144
DeprecationWarning,
11011145
stacklevel=2,
11021146
)
1103-
return self.psd_mockups
1147+
return self._earlier_psd_mockups
11041148

11051149
@mockups.setter
11061150
def mockups(self, value: object) -> None:
1107-
"""Assignment still works so existing test doubles keep running."""
1108-
self.psd_mockups = value # type: ignore[assignment]
1151+
"""Assignment still works so existing test doubles keep running.
1152+
1153+
What is written here is what this name reads back, and only this name:
1154+
:attr:`psd_mockups` is left where it is. A caller that saves this
1155+
accessor, swaps in a double and writes the saved value back therefore
1156+
puts both accessors exactly where they started.
1157+
"""
1158+
self._earlier_psd_mockups = value # type: ignore[assignment]
1159+
1160+
@mockups.deleter
1161+
def mockups(self) -> None:
1162+
"""``del client.mockups`` drops the override and restores the default.
1163+
1164+
The accessor goes back to the one the client was built with, still
1165+
pinned to the endpoint this name has always called.
1166+
``mock.patch.object`` deletes the attribute when its block ends, so
1167+
this is the way back from a patched double.
1168+
"""
1169+
self._earlier_psd_mockups = _AsyncPsdMockupsResource(
1170+
self._transport, base=EARLIER_PSD_MOCKUPS_PATH
1171+
)
11091172

11101173
async def close(self) -> None:
11111174
"""Close the underlying HTTP connection pool."""

0 commit comments

Comments
 (0)