Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .spec-sha256
Original file line number Diff line number Diff line change
@@ -1 +1 @@
06a7c92e23df7c44f8898a2b18abec43aa5c14b8f12ea10e1ce65a94a14a0fb4
e8f81ea407d93977cf0612e9d7e4bbda4b86c4d25de84bf44152b2ff868cde03
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ from controlresell.generated.models import ItemCreateRequest, ItemUpdateRequest,
client.items.create(ItemCreateRequest(name="Jacket")) # client.items.update(42, ItemUpdateRequest(...)) patches

client.items.publish(42, PublishRequest(account_ids=["<account id>"]))
photo = client.files.upload(open("jacket.jpg", "rb").read(), "jacket.jpg", "image/jpeg") # registers the photo and PUTs its bytes; files.register only registers it, files.get reads it (status is PENDING until its bytes are stored)
client.items.create(ItemCreateRequest(name="Jacket", photo_ids=[photo.id]))
client.items.publish(42, PublishRequest(account_ids=["<account id>"], scheduled_at=datetime(2026, 11, 2, 10, 0, tzinfo=timezone.utc))) # publish, price_drop and republish take an optional scheduled_at, at most 30 days ahead; without it they run now
client.conversations.send_message("<conversation id>", SendMessageRequest(text="Hello!"))

Expand Down
48 changes: 48 additions & 0 deletions src/controlresell/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
list_catalog_sizes,
)
from .generated.api.customers import get_customer, list_customers
from .generated.api.files import get_file, upload_file
from .generated.api.favorites import list_favorites, list_item_favorites
from .generated.api.webhooks import (
create_webhook_endpoint,
Expand Down Expand Up @@ -82,6 +83,9 @@
Customer,
CustomerPage,
Favorite,
File,
FileUpload,
FileUploadRequest,
FavoritePage,
Item,
ItemCreateRequest,
Expand Down Expand Up @@ -153,6 +157,12 @@ def __init__(
key = api_key or os.environ.get(API_KEY_VARIABLE)
if not key:
raise ValueError(f"No API key: pass api_key or set {API_KEY_VARIABLE}.")
# The bytes of a file go to the storage address, which is signed: no API key there.
self._storage = httpx.Client(
timeout=httpx.Timeout(timeout) if timeout is not None else None,
headers={"User-Agent": f"controlresell-python/{__version__}"},
transport=RetryTransport(transport or httpx.HTTPTransport(), retry),
)
self._client = AuthenticatedClient(
base_url=base_url,
token=key,
Expand All @@ -166,6 +176,7 @@ def __init__(
self.conversations = _Conversations(self)
self.orders = _Orders(self)
self.customers = _Customers(self)
self.files = _Files(self)
self.favorites = _Favorites(self)
self.catalogs = _Catalogs(self)
self.webhooks = _Webhooks(self)
Expand All @@ -177,6 +188,7 @@ def http(self) -> httpx.Client:

def close(self) -> None:
self._client.get_httpx_client().close()
self._storage.close()

def __enter__(self) -> ControlResell:
return self
Expand Down Expand Up @@ -463,6 +475,42 @@ def iter(
)


class _Files:
"""The photos of the seller's library: register one, send its bytes, read it."""

def __init__(self, sdk: ControlResell) -> None:
self._sdk = sdk

def register(
self, filename: str, content_type: str, size: int, *, idempotency_key: str | None = None
) -> FileUpload:
"""Registers a file (jpeg, png, webp, heic or heif, at most 20 MiB): its identifier and the address where to
``PUT`` its bytes within 30 minutes (needs ``items:write``)."""
body = FileUploadRequest(filename=filename, content_type=content_type, size=size)
return self._sdk._call(upload_file, body=body, idempotency_key=_key(idempotency_key)) # noqa: SLF001

def get(self, id: UUID | str) -> File:
"""One file of the library, with a signed link to read it; ``status`` is ``PENDING`` until its bytes are stored."""
return self._sdk._call(get_file, id=UUID(str(id))) # noqa: SLF001

def upload(
self, data: bytes, filename: str, content_type: str, *, idempotency_key: str | None = None
) -> FileUpload:
"""Registers a file and sends its bytes (an HTTP ``PUT`` with the same ``Content-Type``, no API key), and returns
the registration: put its ``id`` in ``photo_ids``. The ``PUT`` is retried like any call; a refused one is raised
as a ``ControlResellError`` (the file stays ``PENDING``)."""
upload = self.register(filename, content_type, len(data), idempotency_key=idempotency_key)
if not upload.upload_url:
raise ValueError(f"The API returned no upload address for file {upload.id}.")
# A network failure comes out of RetryTransport as a ControlResellConnectionError.
response = self._sdk._storage.put( # noqa: SLF001
upload.upload_url, content=data, headers={"Content-Type": content_type}
)
if not response.is_success:
raise error_of(response.status_code, None, response.headers)
return upload


class _Customers:
"""The other parties of the seller's sales and purchases (needs the ``customers:read`` scope)."""

Expand Down
1 change: 1 addition & 0 deletions src/controlresell/generated/api/files/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Contains endpoint functions for accessing the API"""
206 changes: 206 additions & 0 deletions src/controlresell/generated/api/files/get_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
from http import HTTPStatus
from typing import Any
from urllib.parse import quote
from uuid import UUID

import httpx

from ...client import AuthenticatedClient, Client
from ...models.error_response import ErrorResponse
from ...models.file import File
from ...types import Response


def _get_kwargs(
id: UUID,
) -> dict[str, Any]:

_kwargs: dict[str, Any] = {
"method": "get",
"url": "/api/public/v1/files/{id}".format(
id=quote(str(id), safe=""),
),
}

return _kwargs


def _parse_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> ErrorResponse | File:
if response.status_code == 200:
response_200 = File.from_dict(response.json())

return response_200

if response.status_code == 400:
response_400 = ErrorResponse.from_dict(response.json())

return response_400

if response.status_code == 401:
response_401 = ErrorResponse.from_dict(response.json())

return response_401

if response.status_code == 402:
response_402 = ErrorResponse.from_dict(response.json())

return response_402

if response.status_code == 403:
response_403 = ErrorResponse.from_dict(response.json())

return response_403

if response.status_code == 404:
response_404 = ErrorResponse.from_dict(response.json())

return response_404

if response.status_code == 429:
response_429 = ErrorResponse.from_dict(response.json())

return response_429

if response.status_code == 500:
response_500 = ErrorResponse.from_dict(response.json())

return response_500

response_default = ErrorResponse.from_dict(response.json())

return response_default


def _build_response(
*, client: AuthenticatedClient | Client, response: httpx.Response
) -> Response[ErrorResponse | File]:
return Response(
status_code=HTTPStatus(response.status_code),
content=response.content,
headers=response.headers,
parsed=_parse_response(client=client, response=response),
)


def sync_detailed(
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Response[ErrorResponse | File]:
"""Read one file of the seller's library

One file of the seller's library, by its identifier, with a signed link to read it (it expires: ask
again rather than keep it). `status` is `PENDING` while the bytes are not on our storage yet.
Requires `items:read`.

Args:
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
Response[ErrorResponse | File]
"""

kwargs = _get_kwargs(
id=id,
)

response = client.get_httpx_client().request(
**kwargs,
)

return _build_response(client=client, response=response)


def sync(
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> ErrorResponse | File | None:
"""Read one file of the seller's library

One file of the seller's library, by its identifier, with a signed link to read it (it expires: ask
again rather than keep it). `status` is `PENDING` while the bytes are not on our storage yet.
Requires `items:read`.

Args:
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
ErrorResponse | File
"""

return sync_detailed(
id=id,
client=client,
).parsed


async def asyncio_detailed(
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> Response[ErrorResponse | File]:
"""Read one file of the seller's library

One file of the seller's library, by its identifier, with a signed link to read it (it expires: ask
again rather than keep it). `status` is `PENDING` while the bytes are not on our storage yet.
Requires `items:read`.

Args:
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
Response[ErrorResponse | File]
"""

kwargs = _get_kwargs(
id=id,
)

response = await client.get_async_httpx_client().request(**kwargs)

return _build_response(client=client, response=response)


async def asyncio(
id: UUID,
*,
client: AuthenticatedClient | Client,
) -> ErrorResponse | File | None:
"""Read one file of the seller's library

One file of the seller's library, by its identifier, with a signed link to read it (it expires: ask
again rather than keep it). `status` is `PENDING` while the bytes are not on our storage yet.
Requires `items:read`.

Args:
id (UUID):

Raises:
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
httpx.TimeoutException: If the request takes longer than Client.timeout.

Returns:
ErrorResponse | File
"""

return (
await asyncio_detailed(
id=id,
client=client,
)
).parsed
Loading
Loading