vecsnap 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vecsnap-0.1.0/.gitignore +5 -0
- vecsnap-0.1.0/PKG-INFO +31 -0
- vecsnap-0.1.0/README.md +18 -0
- vecsnap-0.1.0/pyproject.toml +31 -0
- vecsnap-0.1.0/tests/test_vecsnap.py +82 -0
- vecsnap-0.1.0/vecsnap/__init__.py +217 -0
- vecsnap-0.1.0/vecsnap/_generated/__init__.py +8 -0
- vecsnap-0.1.0/vecsnap/_generated/api/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/account/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/account/get_credits.py +180 -0
- vecsnap-0.1.0/vecsnap/_generated/api/account/get_limits.py +180 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/create_conversion_variant.py +250 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/create_palette.py +209 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/delete_palette.py +210 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/get_conversion_paints.py +231 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/get_conversion_palette.py +323 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/list_conversion_variants.py +216 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/list_palettes.py +172 -0
- vecsnap-0.1.0/vecsnap/_generated/api/colors/update_palette.py +231 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/cancel_conversion.py +222 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/create_conversion.py +263 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/delete_conversion.py +218 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/get_conversion.py +214 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/get_conversion_feedback.py +212 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/list_conversions.py +254 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/put_conversion_feedback.py +237 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/unlock_conversion.py +227 -0
- vecsnap-0.1.0/vecsnap/_generated/api/conversions/watch_conversion.py +224 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/create_image_conversion.py +276 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/delete_image.py +218 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/delete_images.py +217 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/erase_image.py +214 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/erase_images.py +221 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/get_image.py +210 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/list_images.py +307 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/rename_image.py +235 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/restore_image.py +210 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/restore_images.py +213 -0
- vecsnap-0.1.0/vecsnap/_generated/api/images/unlock_images.py +227 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/download_conversion.py +327 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/get_conversion_facts.py +227 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/get_conversion_original.py +230 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/get_conversion_shapes.py +252 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/get_conversion_tiles.py +244 -0
- vecsnap-0.1.0/vecsnap/_generated/api/results/preview_conversion.py +278 -0
- vecsnap-0.1.0/vecsnap/_generated/api/uploads/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/uploads/create_upload.py +221 -0
- vecsnap-0.1.0/vecsnap/_generated/api/vectorize/__init__.py +1 -0
- vecsnap-0.1.0/vecsnap/_generated/api/vectorize/vectorize.py +298 -0
- vecsnap-0.1.0/vecsnap/_generated/client.py +280 -0
- vecsnap-0.1.0/vecsnap/_generated/errors.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/__init__.py +293 -0
- vecsnap-0.1.0/vecsnap/_generated/models/conversion.py +325 -0
- vecsnap-0.1.0/vecsnap/_generated/models/conversion_allowance.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/conversion_options.py +65 -0
- vecsnap-0.1.0/vecsnap/_generated/models/conversion_result.py +107 -0
- vecsnap-0.1.0/vecsnap/_generated/models/conversion_status.py +23 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_body.py +144 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_body_options.py +46 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_body_preset.py +15 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_variant_body.py +53 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_variant_body_colors.py +48 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_variant_response_200.py +90 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_conversion_variant_response_200_colors.py +48 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_image_conversion_body.py +73 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_image_conversion_body_options.py +46 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_palette_body.py +51 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_upload_body.py +56 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_upload_body_content_type.py +43 -0
- vecsnap-0.1.0/vecsnap/_generated/models/create_upload_response_200.py +94 -0
- vecsnap-0.1.0/vecsnap/_generated/models/created_conversion.py +338 -0
- vecsnap-0.1.0/vecsnap/_generated/models/created_conversion_allowance.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/created_conversion_status.py +23 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_conversion_response_200.py +68 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_conversion_response_200_deleted.py +20 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_images_body.py +52 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_images_response_200.py +82 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_images_response_200_outcomes_item.py +78 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_images_response_200_outcomes_item_outcome.py +24 -0
- vecsnap-0.1.0/vecsnap/_generated/models/delete_palette_response_200.py +71 -0
- vecsnap-0.1.0/vecsnap/_generated/models/download_conversion_as.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/download_conversion_response_200.py +86 -0
- vecsnap-0.1.0/vecsnap/_generated/models/download_conversion_size.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/erase_image_response_200.py +62 -0
- vecsnap-0.1.0/vecsnap/_generated/models/erase_images_body.py +65 -0
- vecsnap-0.1.0/vecsnap/_generated/models/erase_images_response_200.py +90 -0
- vecsnap-0.1.0/vecsnap/_generated/models/erase_images_response_200_outcomes_item.py +78 -0
- vecsnap-0.1.0/vecsnap/_generated/models/erase_images_response_200_outcomes_item_outcome.py +23 -0
- vecsnap-0.1.0/vecsnap/_generated/models/error.py +69 -0
- vecsnap-0.1.0/vecsnap/_generated/models/error_error.py +81 -0
- vecsnap-0.1.0/vecsnap/_generated/models/failure.py +81 -0
- vecsnap-0.1.0/vecsnap/_generated/models/format_.py +136 -0
- vecsnap-0.1.0/vecsnap/_generated/models/format_background_type_0.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/format_profile.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_facts_response_200.py +110 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_facts_response_200_colors_item.py +93 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_facts_response_200_colors_item_roles_item.py +24 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_facts_response_200_summary.py +150 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_feedback_response_200.py +98 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_feedback_response_200_verdict_type_0.py +125 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_feedback_response_200_verdict_type_0_reasons_item.py +26 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_feedback_response_200_verdict_type_0_vote.py +20 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_paints_response_200.py +136 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_paints_response_200_paints_item.py +70 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_palette_response_200_type_0.py +128 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_palette_response_200_type_0_counts_item_type_0.py +99 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_palette_response_200_type_0_counts_item_type_0_mapping.py +48 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_palette_response_200_type_0_counts_item_type_1.py +70 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_palette_response_200_type_1.py +118 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_palette_response_200_type_1_mapping.py +48 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_shapes_response_200.py +120 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_shapes_response_200_rows_item.py +129 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_tiles_body.py +137 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_tiles_response_200.py +146 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_conversion_tiles_response_200_tiles_item.py +78 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_credits_response_200.py +83 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_credits_response_200_tier.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/get_limits_response_200.py +87 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image.py +280 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_detail.py +297 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_detail_state.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_list.py +144 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_state.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_version.py +354 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_version_allowance.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/image_version_status.py +23 -0
- vecsnap-0.1.0/vecsnap/_generated/models/library_counts.py +95 -0
- vecsnap-0.1.0/vecsnap/_generated/models/link.py +71 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_conversion_variants_response_200.py +120 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_conversion_variants_response_200_data_item.py +93 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_conversion_variants_response_200_data_item_colors.py +48 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_conversions_live.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_conversions_response_200.py +114 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_images_deleted.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_images_filter.py +18 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_images_sort.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/list_palettes_response_200.py +114 -0
- vecsnap-0.1.0/vecsnap/_generated/models/palette.py +104 -0
- vecsnap-0.1.0/vecsnap/_generated/models/picture.py +81 -0
- vecsnap-0.1.0/vecsnap/_generated/models/picture_of.py +16 -0
- vecsnap-0.1.0/vecsnap/_generated/models/preview_conversion_layer.py +15 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_body.py +105 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_body_reasons_item.py +26 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_body_vote.py +18 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_response_200.py +72 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_response_200_verdict.py +123 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_response_200_verdict_reasons_item.py +26 -0
- vecsnap-0.1.0/vecsnap/_generated/models/put_conversion_feedback_response_200_verdict_vote.py +20 -0
- vecsnap-0.1.0/vecsnap/_generated/models/rename_image_body.py +50 -0
- vecsnap-0.1.0/vecsnap/_generated/models/restore_images_body.py +52 -0
- vecsnap-0.1.0/vecsnap/_generated/models/restore_images_response_200.py +82 -0
- vecsnap-0.1.0/vecsnap/_generated/models/restore_images_response_200_outcomes_item.py +78 -0
- vecsnap-0.1.0/vecsnap/_generated/models/restore_images_response_200_outcomes_item_outcome.py +23 -0
- vecsnap-0.1.0/vecsnap/_generated/models/unlock_conversion_response_200.py +70 -0
- vecsnap-0.1.0/vecsnap/_generated/models/unlock_images_body.py +52 -0
- vecsnap-0.1.0/vecsnap/_generated/models/unlock_images_response_200.py +79 -0
- vecsnap-0.1.0/vecsnap/_generated/models/update_palette_body.py +51 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorize_files_body.py +178 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorize_files_body_options.py +46 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorize_files_body_preset.py +15 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorize_json_body.py +119 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorize_json_body_options.py +46 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorize_json_body_preset.py +15 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorized.py +345 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorized_allowance.py +17 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorized_files.py +52 -0
- vecsnap-0.1.0/vecsnap/_generated/models/vectorized_status.py +23 -0
- vecsnap-0.1.0/vecsnap/_generated/models/webhook_event.py +122 -0
- vecsnap-0.1.0/vecsnap/_generated/models/webhook_event_type.py +19 -0
- vecsnap-0.1.0/vecsnap/_generated/types.py +54 -0
vecsnap-0.1.0/.gitignore
ADDED
vecsnap-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vecsnap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The VecSnap API from Python: turn an image into a clean SVG in one call.
|
|
5
|
+
Project-URL: Homepage, https://vecsnap.com/docs/sdks/python
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: image,sdk,svg,vecsnap,vector,vectorize
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: attrs>=23
|
|
10
|
+
Requires-Dist: httpx<1,>=0.27
|
|
11
|
+
Requires-Dist: python-dateutil>=2.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# vecsnap — the VecSnap API from Python
|
|
15
|
+
|
|
16
|
+
Turn an image into a clean SVG in one call.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from vecsnap import VecSnap
|
|
20
|
+
|
|
21
|
+
vecsnap = VecSnap() # reads VECSNAP_API_KEY: vs_test_… is free (a sample result), vs_live_… converts for real
|
|
22
|
+
result = vecsnap.vectorize(file=open("logo.png", "rb").read(), filename="logo.png")
|
|
23
|
+
open("logo.svg", "wb").write(vecsnap.download(result))
|
|
24
|
+
print(result["result"]["anchorPoints"])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
A conversion that outlasts one call is waited for. Every refusal is a `VecSnapError` with its `code`
|
|
28
|
+
(vecsnap.com/docs/errors). Check a webhook your server received with `verify_webhook(raw_body, headers, secret)`.
|
|
29
|
+
|
|
30
|
+
`vecsnap._generated` is generated from the API's document — every operation a key may call — and never edited by hand.
|
|
31
|
+
Docs: https://vecsnap.com/docs/sdks/python
|
vecsnap-0.1.0/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# vecsnap — the VecSnap API from Python
|
|
2
|
+
|
|
3
|
+
Turn an image into a clean SVG in one call.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from vecsnap import VecSnap
|
|
7
|
+
|
|
8
|
+
vecsnap = VecSnap() # reads VECSNAP_API_KEY: vs_test_… is free (a sample result), vs_live_… converts for real
|
|
9
|
+
result = vecsnap.vectorize(file=open("logo.png", "rb").read(), filename="logo.png")
|
|
10
|
+
open("logo.svg", "wb").write(vecsnap.download(result))
|
|
11
|
+
print(result["result"]["anchorPoints"])
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A conversion that outlasts one call is waited for. Every refusal is a `VecSnapError` with its `code`
|
|
15
|
+
(vecsnap.com/docs/errors). Check a webhook your server received with `verify_webhook(raw_body, headers, secret)`.
|
|
16
|
+
|
|
17
|
+
`vecsnap._generated` is generated from the API's document — every operation a key may call — and never edited by hand.
|
|
18
|
+
Docs: https://vecsnap.com/docs/sdks/python
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vecsnap"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "The VecSnap API from Python: turn an image into a clean SVG in one call."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx>=0.27,<1",
|
|
10
|
+
"attrs>=23",
|
|
11
|
+
"python-dateutil>=2.8",
|
|
12
|
+
]
|
|
13
|
+
keywords = ["vecsnap", "svg", "vectorize", "vector", "image", "sdk"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://vecsnap.com/docs/sdks/python"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = ["pytest>=8"]
|
|
20
|
+
|
|
21
|
+
# pytest roots HERE: without its own section it climbs to the repository's `pyproject.toml` and loads the engine's
|
|
22
|
+
# `conftest.py` (the gate's read check refuses the engine files that pulls into the web's `static` unit).
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["hatchling>=1.25"]
|
|
28
|
+
build-backend = "hatchling.build"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["vecsnap"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""``vecsnap`` on a fake transport — what the hand-written layer adds: ``vectorize`` waits through a 202 and asks again
|
|
2
|
+
with the same idempotency key, a refusal is a ``VecSnapError``, a webhook is checked by the Standard's own known answer."""
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from vecsnap import VecSnap, VecSnapError, WebhookVerificationError, sign_webhook, verify_webhook
|
|
9
|
+
|
|
10
|
+
VECTORIZED = {"object": "conversion", "id": "c1", "status": "completed", "files": {"web": "https://files.example/web.svg"}}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def client(answers):
|
|
14
|
+
calls = []
|
|
15
|
+
|
|
16
|
+
def handle(request: httpx.Request) -> httpx.Response:
|
|
17
|
+
calls.append(request)
|
|
18
|
+
return answers.pop(0)(request)
|
|
19
|
+
|
|
20
|
+
vecsnap = VecSnap(api_key="vs_test_x", base_url="https://api.example/v1", http=httpx.Client(transport=httpx.MockTransport(handle)))
|
|
21
|
+
return vecsnap, calls
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def answer(status, body, headers=None):
|
|
25
|
+
return lambda request: httpx.Response(status, json=body, headers=headers or {})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_a_file_goes_as_the_form_with_the_key_and_an_idempotency_key():
|
|
29
|
+
vecsnap, calls = client([answer(200, VECTORIZED)])
|
|
30
|
+
assert vecsnap.vectorize(file=b"png", filename="logo.png", options={"maxColors": 4})["files"]["web"]
|
|
31
|
+
request = calls[0]
|
|
32
|
+
assert request.url == "https://api.example/v1/vectorize"
|
|
33
|
+
assert request.headers["authorization"] == "Bearer vs_test_x"
|
|
34
|
+
assert len(request.headers["idempotency-key"]) == 36
|
|
35
|
+
body = request.read().decode()
|
|
36
|
+
assert 'filename="logo.png"' in body and '{"maxColors": 4}' in body
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_it_waits_through_a_202_then_asks_each_format_for_its_link(monkeypatch):
|
|
40
|
+
monkeypatch.setattr("time.sleep", lambda s: None)
|
|
41
|
+
processing = {"object": "conversion", "id": "c1", "status": "processing"}
|
|
42
|
+
done = {**processing, "status": "completed", "unlocked": True, "result": {"formats": [{"id": "web"}]}}
|
|
43
|
+
vecsnap, calls = client([
|
|
44
|
+
answer(202, processing),
|
|
45
|
+
answer(429, {"error": {"code": "rate_limited", "message": "slow"}}, {"retry-after": "1"}),
|
|
46
|
+
answer(200, done),
|
|
47
|
+
answer(200, {"url": "https://files.example/web.svg", "expiresIn": 3600}),
|
|
48
|
+
])
|
|
49
|
+
result = vecsnap.vectorize(url="https://example.com/logo.png")
|
|
50
|
+
assert result["files"] == {"web": "https://files.example/web.svg"}
|
|
51
|
+
assert [f"{c.method} {c.url.path}" for c in calls] == ["POST /v1/vectorize", "GET /v1/conversions/c1", "GET /v1/conversions/c1", "GET /v1/conversions/c1/download"]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_a_failure_past_the_202_names_the_conversion_and_the_key(monkeypatch):
|
|
55
|
+
monkeypatch.setattr("time.sleep", lambda s: None)
|
|
56
|
+
vecsnap, _ = client([answer(202, {"object": "conversion", "id": "c1", "status": "processing"})])
|
|
57
|
+
with pytest.raises(VecSnapError) as raised:
|
|
58
|
+
vecsnap.vectorize(input_key="inputs/x.png", idempotency_key="my-key", timeout_seconds=0)
|
|
59
|
+
assert (raised.value.code, raised.value.conversion_id, raised.value.idempotency_key) == ("timeout", "c1", "my-key")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_a_refusal_is_a_vecsnap_error_with_its_code():
|
|
63
|
+
vecsnap, _ = client([answer(402, {"error": {"code": "insufficient_credits", "message": "buy credits", "requestId": "req_1"}})])
|
|
64
|
+
with pytest.raises(VecSnapError) as raised:
|
|
65
|
+
vecsnap.vectorize(input_key="inputs/x.png")
|
|
66
|
+
assert (raised.value.code, raised.value.status, raised.value.request_id) == ("insufficient_credits", 402, "req_1")
|
|
67
|
+
with pytest.raises(VecSnapError):
|
|
68
|
+
vecsnap.vectorize()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# standard-webhooks `libraries/python/tests/test_webhooks.py` `test_sign_function` — the specification's own vector.
|
|
72
|
+
SECRET, ID, AT, BODY = "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw", "msg_p5jXN8AQM9LWM0D4loKWxJek", 1614265330, '{"test": 2432232314}'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_webhooks_known_answer_and_refusals():
|
|
76
|
+
assert sign_webhook(BODY, SECRET, ID, AT) == "v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="
|
|
77
|
+
headers = {"Webhook-Id": ID, "webhook-timestamp": str(AT), "webhook-signature": "v1,AAAA v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE="}
|
|
78
|
+
assert verify_webhook(BODY, headers, SECRET, now=AT) == {"test": 2432232314}
|
|
79
|
+
for body, now, reason in [(BODY + " ", AT, "bad_signature"), (BODY, AT + 301, "stale")]:
|
|
80
|
+
with pytest.raises(WebhookVerificationError) as raised:
|
|
81
|
+
verify_webhook(body, headers, SECRET, now=now)
|
|
82
|
+
assert raised.value.reason == reason
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""``vecsnap`` — the VecSnap API from Python (``developers.md`` § 8; audit ``developer-platform-2026-10-23`` § 13).
|
|
2
|
+
|
|
3
|
+
from vecsnap import VecSnap
|
|
4
|
+
vecsnap = VecSnap() # reads VECSNAP_API_KEY
|
|
5
|
+
result = vecsnap.vectorize(file=open("logo.png", "rb").read(), filename="logo.png")
|
|
6
|
+
open("logo.svg", "wb").write(vecsnap.download(result))
|
|
7
|
+
|
|
8
|
+
Every operation a key may call is GENERATED from the API's own document (``vecsnap._generated``, never edited by hand —
|
|
9
|
+
``npm --prefix web run gen:clients``) and reachable through ``vecsnap.api`` (its ``Client``, the key set). This module adds
|
|
10
|
+
what generation cannot: ``vectorize`` sends a file as a form and WAITS through a 202, every refusal is a ``VecSnapError``
|
|
11
|
+
with its code, and ``verify_webhook`` checks an event your server received.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import base64
|
|
16
|
+
import hashlib
|
|
17
|
+
import hmac
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import time
|
|
22
|
+
import uuid
|
|
23
|
+
from typing import Any, Mapping, Optional
|
|
24
|
+
|
|
25
|
+
import httpx
|
|
26
|
+
|
|
27
|
+
from ._generated import AuthenticatedClient
|
|
28
|
+
|
|
29
|
+
__all__ = ["VecSnap", "VecSnapError", "WebhookVerificationError", "verify_webhook", "sign_webhook", "DEFAULT_BASE_URL"]
|
|
30
|
+
|
|
31
|
+
DEFAULT_BASE_URL = "https://api.vecsnap.com/v1"
|
|
32
|
+
ENDED = {"completed", "failed", "expired", "canceled"}
|
|
33
|
+
#: How far ``webhook-timestamp`` may be from now: the reference libraries' five minutes, either way.
|
|
34
|
+
WEBHOOK_TOLERANCE_SECONDS = 5 * 60
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class VecSnapError(Exception):
|
|
38
|
+
"""A refusal of the API: its ``code`` (branch on it — every code is on vecsnap.com/docs/errors), its HTTP ``status``,
|
|
39
|
+
the ``message`` for a person, the ``request_id`` to quote, and for 429 how many seconds to wait (``retry_after``)."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, code: str, message: str, status: int = 0, request_id: Optional[str] = None, retry_after: Optional[float] = None):
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.code = code
|
|
44
|
+
self.message = message
|
|
45
|
+
self.status = status
|
|
46
|
+
self.request_id = request_id
|
|
47
|
+
self.retry_after = retry_after
|
|
48
|
+
#: For a ``vectorize`` that stopped part way: the conversion it made and the key it sent — call again with it.
|
|
49
|
+
self.conversion_id: Optional[str] = None
|
|
50
|
+
self.idempotency_key: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def from_response(cls, response: httpx.Response) -> "VecSnapError":
|
|
54
|
+
try:
|
|
55
|
+
retry_after: Optional[float] = float(response.headers["retry-after"])
|
|
56
|
+
except (KeyError, ValueError):
|
|
57
|
+
retry_after = None
|
|
58
|
+
try:
|
|
59
|
+
error = response.json().get("error") or {}
|
|
60
|
+
except ValueError:
|
|
61
|
+
error = {}
|
|
62
|
+
if isinstance(error.get("code"), str):
|
|
63
|
+
return cls(error["code"], str(error.get("message", error["code"])), response.status_code, error.get("requestId") or response.headers.get("x-request-id"), retry_after)
|
|
64
|
+
return cls("unexpected_response", f"the API answered {response.status_code} without an error body", response.status_code, response.headers.get("x-request-id"), retry_after)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class VecSnap:
|
|
68
|
+
"""A client for one API key — ``vs_test_…`` (free: a sample result) or ``vs_live_…``."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, http: Optional[httpx.Client] = None, timeout: float = 90.0):
|
|
71
|
+
key = api_key or os.environ.get("VECSNAP_API_KEY")
|
|
72
|
+
if not key:
|
|
73
|
+
raise VecSnapError("missing_api_key", "no API key: pass api_key= or set VECSNAP_API_KEY (vecsnap.com/app/developers)")
|
|
74
|
+
# A proxy or a test server: base_url=, else VECSNAP_BASE_URL, else the API.
|
|
75
|
+
self._base_url = (base_url or os.environ.get("VECSNAP_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
|
|
76
|
+
self._http = http or httpx.Client(timeout=timeout)
|
|
77
|
+
self._headers = {"authorization": f"Bearer {key}", "accept": "application/json"}
|
|
78
|
+
#: The generated client, the key set: ``from vecsnap._generated.api.conversions import get_conversion``,
|
|
79
|
+
#: then ``get_conversion.sync(id=…, client=vecsnap.api)``.
|
|
80
|
+
self.api = AuthenticatedClient(base_url=self._base_url, token=key, timeout=httpx.Timeout(timeout))
|
|
81
|
+
|
|
82
|
+
def vectorize(
|
|
83
|
+
self,
|
|
84
|
+
*,
|
|
85
|
+
file: Optional[bytes] = None,
|
|
86
|
+
url: Optional[str] = None,
|
|
87
|
+
input_key: Optional[str] = None,
|
|
88
|
+
filename: Optional[str] = None,
|
|
89
|
+
preset: Optional[str] = None,
|
|
90
|
+
options: Optional[Mapping[str, Any]] = None,
|
|
91
|
+
idempotency_key: Optional[str] = None,
|
|
92
|
+
wait: int = 30,
|
|
93
|
+
timeout_seconds: float = 600,
|
|
94
|
+
) -> dict:
|
|
95
|
+
"""Convert one image and wait for its SVG: the conversion and a link to each SVG file (``files``). A conversion that
|
|
96
|
+
outlasts one call (202) is waited for here — read again with backoff, its files then asked for. Raises
|
|
97
|
+
``VecSnapError`` (``insufficient_credits``, ``timeout``, …) — past the 202 with ``conversion_id`` and
|
|
98
|
+
``idempotency_key``: call again with that key to pick the same conversion up."""
|
|
99
|
+
if sum(x is not None for x in (file, url, input_key)) != 1:
|
|
100
|
+
raise VecSnapError("invalid_request", 'give exactly one of "file", "url" or "input_key"')
|
|
101
|
+
key = idempotency_key or str(uuid.uuid4())
|
|
102
|
+
sent = dict(file=file, url=url, input_key=input_key, filename=filename, preset=preset, options=options)
|
|
103
|
+
deadline = time.monotonic() + timeout_seconds
|
|
104
|
+
status, body = self._vectorize_once(sent, key, wait)
|
|
105
|
+
if status == 200:
|
|
106
|
+
return body
|
|
107
|
+
# A 202: the conversion exists and finishes on its own. Wait for it — and if this call cannot, say which
|
|
108
|
+
# conversion and which key, so a retry picks the SAME one up (never a second charge).
|
|
109
|
+
conversion_id = body["id"]
|
|
110
|
+
try:
|
|
111
|
+
delay = 1.0
|
|
112
|
+
while True:
|
|
113
|
+
if time.monotonic() + delay > deadline:
|
|
114
|
+
raise VecSnapError("timeout", f"conversion {conversion_id} had not ended within the time given")
|
|
115
|
+
time.sleep(delay)
|
|
116
|
+
delay = min(delay * 1.6, 10.0)
|
|
117
|
+
try:
|
|
118
|
+
conversion = self.conversion(conversion_id)
|
|
119
|
+
except VecSnapError as error:
|
|
120
|
+
if error.status == 429 or error.status >= 500: # wait longer; never give the conversion up for it
|
|
121
|
+
delay = max(delay, error.retry_after or 0)
|
|
122
|
+
continue
|
|
123
|
+
raise
|
|
124
|
+
if conversion["status"] in ENDED:
|
|
125
|
+
# Its files from the conversion itself — never the request sent again, whose URL may be gone.
|
|
126
|
+
return self._with_files(conversion)
|
|
127
|
+
except VecSnapError as error:
|
|
128
|
+
error.conversion_id, error.idempotency_key = conversion_id, key
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
def conversion(self, conversion_id: str) -> dict:
|
|
132
|
+
"""One conversion as it stands. Raises ``VecSnapError`` ``not_found``."""
|
|
133
|
+
response = self._http.get(f"{self._base_url}/conversions/{conversion_id}", headers=self._headers)
|
|
134
|
+
if response.status_code != 200:
|
|
135
|
+
raise VecSnapError.from_response(response)
|
|
136
|
+
return response.json()
|
|
137
|
+
|
|
138
|
+
def _with_files(self, conversion: dict) -> dict:
|
|
139
|
+
"""An ended conversion with a link to each of its files (an image not unlocked has none), as ``vectorize`` answers."""
|
|
140
|
+
files: dict = {}
|
|
141
|
+
expires_in = None
|
|
142
|
+
if conversion["status"] == "completed" and conversion.get("unlocked"):
|
|
143
|
+
for fmt in (conversion.get("result") or {}).get("formats") or []:
|
|
144
|
+
response = self._http.get(f"{self._base_url}/conversions/{conversion['id']}/download", headers=self._headers, params={"format": fmt["id"]})
|
|
145
|
+
if response.status_code != 200:
|
|
146
|
+
raise VecSnapError.from_response(response)
|
|
147
|
+
link = response.json()
|
|
148
|
+
files[fmt["id"]] = link["url"]
|
|
149
|
+
expires_in = link["expiresIn"] if expires_in is None else min(expires_in, link["expiresIn"])
|
|
150
|
+
expires_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() + (expires_in or 0)))
|
|
151
|
+
return {**conversion, "files": files, "filesExpireAt": expires_at}
|
|
152
|
+
|
|
153
|
+
def download(self, result: Mapping[str, Any], format: str = "web") -> bytes:
|
|
154
|
+
"""The bytes of a vectorized image's SVG (``web`` by default; ``design``, …: each of ``files``)."""
|
|
155
|
+
link = (result.get("files") or {}).get(format)
|
|
156
|
+
if not link:
|
|
157
|
+
raise VecSnapError("format_unavailable", f'no "{format}" file: {", ".join(result.get("files") or {}) or "none"} (an image not unlocked has none)')
|
|
158
|
+
response = self._http.get(link)
|
|
159
|
+
if response.status_code != 200:
|
|
160
|
+
raise VecSnapError("download_failed", f"the file answered {response.status_code} (links last until filesExpireAt)", response.status_code)
|
|
161
|
+
return response.content
|
|
162
|
+
|
|
163
|
+
def _vectorize_once(self, sent: Mapping[str, Any], key: str, wait: int) -> tuple[int, dict]:
|
|
164
|
+
headers = {**self._headers, "idempotency-key": key}
|
|
165
|
+
if sent["file"] is not None:
|
|
166
|
+
data = {"wait": str(wait)}
|
|
167
|
+
for field in ("filename", "preset"):
|
|
168
|
+
if sent[field] is not None:
|
|
169
|
+
data[field] = sent[field]
|
|
170
|
+
if sent["options"] is not None:
|
|
171
|
+
data["options"] = json.dumps(dict(sent["options"]))
|
|
172
|
+
files = {"file": (sent["filename"] or "image", sent["file"])}
|
|
173
|
+
response = self._http.post(f"{self._base_url}/vectorize", headers=headers, data=data, files=files)
|
|
174
|
+
else:
|
|
175
|
+
payload = {"wait": wait, "url": sent["url"], "inputKey": sent["input_key"], "filename": sent["filename"], "preset": sent["preset"], "options": sent["options"]}
|
|
176
|
+
response = self._http.post(f"{self._base_url}/vectorize", headers=headers, json={k: v for k, v in payload.items() if v is not None})
|
|
177
|
+
if response.status_code in (200, 202):
|
|
178
|
+
return response.status_code, response.json()
|
|
179
|
+
raise VecSnapError.from_response(response)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class WebhookVerificationError(Exception):
|
|
183
|
+
"""Why an event was refused: ``missing_headers``, ``malformed_timestamp``, ``stale``, ``bad_signature`` or ``bad_secret``."""
|
|
184
|
+
|
|
185
|
+
def __init__(self, reason: str, message: str):
|
|
186
|
+
super().__init__(message)
|
|
187
|
+
self.reason = reason
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _secret_key(secret: str) -> bytes:
|
|
191
|
+
match = re.fullmatch(r"(?:v1,)?whsec_([A-Za-z0-9+/]+={0,2})", secret.strip())
|
|
192
|
+
if not match:
|
|
193
|
+
raise WebhookVerificationError("bad_secret", "a webhook secret is whsec_ followed by base64")
|
|
194
|
+
return base64.b64decode(match.group(1))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def sign_webhook(body: str, secret: str, webhook_id: str, timestamp: int) -> str:
|
|
198
|
+
"""``v1,<base64>`` of an event signed with ``secret`` — what VecSnap sends; your tests can sign their own with it."""
|
|
199
|
+
mac = hmac.new(_secret_key(secret), f"{webhook_id}.{timestamp}.{body}".encode(), hashlib.sha256).digest()
|
|
200
|
+
return "v1," + base64.b64encode(mac).decode()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def verify_webhook(body: str, headers: Mapping[str, str], secret: str, now: Optional[float] = None) -> dict:
|
|
204
|
+
"""The event, once its Standard Webhooks signature and timestamp are checked — or ``WebhookVerificationError``.
|
|
205
|
+
Pass the RAW body, as it arrived. Dedupe on ``event["id"]``: an event may come more than once."""
|
|
206
|
+
lower = {k.lower(): v for k, v in headers.items()}
|
|
207
|
+
webhook_id, timestamp, signatures = lower.get("webhook-id"), lower.get("webhook-timestamp"), lower.get("webhook-signature")
|
|
208
|
+
if not webhook_id or not timestamp or not signatures:
|
|
209
|
+
raise WebhookVerificationError("missing_headers", "webhook-id, webhook-timestamp and webhook-signature are required")
|
|
210
|
+
if not re.fullmatch(r"\d{1,12}", timestamp):
|
|
211
|
+
raise WebhookVerificationError("malformed_timestamp", "webhook-timestamp is not a number of seconds")
|
|
212
|
+
if abs(int(now if now is not None else time.time()) - int(timestamp)) > WEBHOOK_TOLERANCE_SECONDS:
|
|
213
|
+
raise WebhookVerificationError("stale", "webhook-timestamp is more than 5 minutes from now: a replay, or a clock to set")
|
|
214
|
+
expected = sign_webhook(body, secret, webhook_id, int(timestamp))
|
|
215
|
+
if not any(hmac.compare_digest(given, expected) for given in signatures.split(" ")):
|
|
216
|
+
raise WebhookVerificationError("bad_signature", "no webhook-signature matches this secret: a forged event, or another endpoint's secret")
|
|
217
|
+
return json.loads(body)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from http import HTTPStatus
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from ... import errors
|
|
7
|
+
from ...client import AuthenticatedClient, Client
|
|
8
|
+
from ...models.error import Error
|
|
9
|
+
from ...models.get_credits_response_200 import GetCreditsResponse200
|
|
10
|
+
from ...types import Response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_kwargs() -> dict[str, Any]:
|
|
14
|
+
|
|
15
|
+
_kwargs: dict[str, Any] = {
|
|
16
|
+
"method": "get",
|
|
17
|
+
"url": "/credits",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return _kwargs
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_response(
|
|
24
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
25
|
+
) -> Error | GetCreditsResponse200 | None:
|
|
26
|
+
if response.status_code == 200:
|
|
27
|
+
response_200 = GetCreditsResponse200.from_dict(response.json())
|
|
28
|
+
|
|
29
|
+
return response_200
|
|
30
|
+
|
|
31
|
+
if response.status_code == 401:
|
|
32
|
+
response_401 = Error.from_dict(response.json())
|
|
33
|
+
|
|
34
|
+
return response_401
|
|
35
|
+
|
|
36
|
+
if response.status_code == 403:
|
|
37
|
+
response_403 = Error.from_dict(response.json())
|
|
38
|
+
|
|
39
|
+
return response_403
|
|
40
|
+
|
|
41
|
+
if response.status_code == 409:
|
|
42
|
+
response_409 = Error.from_dict(response.json())
|
|
43
|
+
|
|
44
|
+
return response_409
|
|
45
|
+
|
|
46
|
+
if response.status_code == 422:
|
|
47
|
+
response_422 = Error.from_dict(response.json())
|
|
48
|
+
|
|
49
|
+
return response_422
|
|
50
|
+
|
|
51
|
+
if response.status_code == 429:
|
|
52
|
+
response_429 = Error.from_dict(response.json())
|
|
53
|
+
|
|
54
|
+
return response_429
|
|
55
|
+
|
|
56
|
+
if response.status_code == 500:
|
|
57
|
+
response_500 = Error.from_dict(response.json())
|
|
58
|
+
|
|
59
|
+
return response_500
|
|
60
|
+
|
|
61
|
+
if response.status_code == 503:
|
|
62
|
+
response_503 = Error.from_dict(response.json())
|
|
63
|
+
|
|
64
|
+
return response_503
|
|
65
|
+
|
|
66
|
+
if client.raise_on_unexpected_status:
|
|
67
|
+
raise errors.UnexpectedStatus(response.status_code, response.content)
|
|
68
|
+
else:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build_response(
|
|
73
|
+
*, client: AuthenticatedClient | Client, response: httpx.Response
|
|
74
|
+
) -> Response[Error | GetCreditsResponse200]:
|
|
75
|
+
return Response(
|
|
76
|
+
status_code=HTTPStatus(response.status_code),
|
|
77
|
+
content=response.content,
|
|
78
|
+
headers=response.headers,
|
|
79
|
+
parsed=_parse_response(client=client, response=response),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def sync_detailed(
|
|
84
|
+
*,
|
|
85
|
+
client: AuthenticatedClient,
|
|
86
|
+
) -> Response[Error | GetCreditsResponse200]:
|
|
87
|
+
"""Retrieve the credit balance
|
|
88
|
+
|
|
89
|
+
Derived from the ledger on every call: there is no balance column, since a cached total drifts under
|
|
90
|
+
partial failure and the customer is the one who finds out. `unlockCost` is what unlocking one image
|
|
91
|
+
costs, published so a client never hardcodes it; `tier` says which caps apply.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
95
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Response[Error | GetCreditsResponse200]
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
kwargs = _get_kwargs()
|
|
102
|
+
|
|
103
|
+
response = client.get_httpx_client().request(
|
|
104
|
+
**kwargs,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return _build_response(client=client, response=response)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def sync(
|
|
111
|
+
*,
|
|
112
|
+
client: AuthenticatedClient,
|
|
113
|
+
) -> Error | GetCreditsResponse200 | None:
|
|
114
|
+
"""Retrieve the credit balance
|
|
115
|
+
|
|
116
|
+
Derived from the ledger on every call: there is no balance column, since a cached total drifts under
|
|
117
|
+
partial failure and the customer is the one who finds out. `unlockCost` is what unlocking one image
|
|
118
|
+
costs, published so a client never hardcodes it; `tier` says which caps apply.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
122
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Error | GetCreditsResponse200
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
return sync_detailed(
|
|
129
|
+
client=client,
|
|
130
|
+
).parsed
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def asyncio_detailed(
|
|
134
|
+
*,
|
|
135
|
+
client: AuthenticatedClient,
|
|
136
|
+
) -> Response[Error | GetCreditsResponse200]:
|
|
137
|
+
"""Retrieve the credit balance
|
|
138
|
+
|
|
139
|
+
Derived from the ledger on every call: there is no balance column, since a cached total drifts under
|
|
140
|
+
partial failure and the customer is the one who finds out. `unlockCost` is what unlocking one image
|
|
141
|
+
costs, published so a client never hardcodes it; `tier` says which caps apply.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
145
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Response[Error | GetCreditsResponse200]
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
kwargs = _get_kwargs()
|
|
152
|
+
|
|
153
|
+
response = await client.get_async_httpx_client().request(**kwargs)
|
|
154
|
+
|
|
155
|
+
return _build_response(client=client, response=response)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def asyncio(
|
|
159
|
+
*,
|
|
160
|
+
client: AuthenticatedClient,
|
|
161
|
+
) -> Error | GetCreditsResponse200 | None:
|
|
162
|
+
"""Retrieve the credit balance
|
|
163
|
+
|
|
164
|
+
Derived from the ledger on every call: there is no balance column, since a cached total drifts under
|
|
165
|
+
partial failure and the customer is the one who finds out. `unlockCost` is what unlocking one image
|
|
166
|
+
costs, published so a client never hardcodes it; `tier` says which caps apply.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True.
|
|
170
|
+
httpx.TimeoutException: If the request takes longer than Client.timeout.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Error | GetCreditsResponse200
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
await asyncio_detailed(
|
|
178
|
+
client=client,
|
|
179
|
+
)
|
|
180
|
+
).parsed
|