plotwork 0.1.0__py3-none-any.whl

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.
plotwork/_request.py ADDED
@@ -0,0 +1,187 @@
1
+ """The one transport every operation calls.
2
+
3
+ It sends a request to the Plotwork API with ``urllib.request`` and reads the answer. A 2xx answer is the result; a mistake in a
4
+ diagram is part of that result (``valid: False``, ``errors`` with line, column and hint), never an exception. Anything else is a
5
+ ``PlotworkApiError`` with the API's stable ``error_code``. A call the API may answer again safely is retried on 429 and 503,
6
+ after the ``Retry-After`` it gives; publishing is never retried.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import math
13
+ import socket
14
+ import time
15
+ import urllib.error
16
+ import urllib.parse
17
+ import urllib.request
18
+ from typing import Any, Callable, Mapping, Optional
19
+
20
+ from ._version import __version__
21
+
22
+ #: The longest the client waits before one retry, whatever ``Retry-After`` says, in seconds.
23
+ MAX_RETRY_WAIT = 30.0
24
+
25
+ #: Opens a ``urllib.request.Request`` and returns the response, called like ``urllib.request.urlopen(request, timeout=...)``.
26
+ #: It may raise ``urllib.error.HTTPError`` for a status that is not 2xx, as ``urlopen`` does, or return that response.
27
+ Opener = Callable[..., Any]
28
+
29
+
30
+ class PlotworkApiError(Exception):
31
+ """The API's error envelope (``ErrorResponse``), or what the client knew when there was none."""
32
+
33
+ #: The HTTP status, or 0 when no answer came (offline, refused, timed out).
34
+ status: int
35
+ #: A stable key such as ``auth.invalid_key``, ``rate_limit.exceeded`` or ``request.timeout``.
36
+ error_code: str
37
+ message: str
38
+ details: Any
39
+ #: The API's trace id, to quote to support.
40
+ request_id: Optional[str]
41
+
42
+ def __init__(self, status: int, error_code: str, message: str, details: Any = None, request_id: Optional[str] = None) -> None:
43
+ super().__init__(message)
44
+ self.status = status
45
+ self.error_code = error_code
46
+ self.message = message
47
+ self.details = details
48
+ self.request_id = request_id
49
+
50
+ def __repr__(self) -> str:
51
+ return f"PlotworkApiError(status={self.status!r}, error_code={self.error_code!r}, message={self.message!r}, request_id={self.request_id!r})"
52
+
53
+
54
+ def retry_delay(retry_after: Optional[str], attempt: int) -> float:
55
+ """How many seconds to wait before retry number ``attempt`` (from 1): the API's ``Retry-After``, else a doubling half second."""
56
+ seconds = math.nan
57
+ if retry_after is not None:
58
+ try:
59
+ seconds = float(retry_after)
60
+ except ValueError:
61
+ seconds = math.nan
62
+ wait = seconds if math.isfinite(seconds) and seconds >= 0 else 0.5 * 2 ** (attempt - 1)
63
+ return min(wait, MAX_RETRY_WAIT)
64
+
65
+
66
+ def _query_value(value: Any) -> str:
67
+ if value is True:
68
+ return "true"
69
+ if value is False:
70
+ return "false"
71
+ return str(value)
72
+
73
+
74
+ def _status_of(response: Any) -> int:
75
+ status = getattr(response, "status", None)
76
+ return int(status if status is not None else response.getcode())
77
+
78
+
79
+ def _header(response: Any, name: str) -> Optional[str]:
80
+ headers = getattr(response, "headers", None)
81
+ if headers is None:
82
+ return None
83
+ value = headers.get(name)
84
+ return None if value is None else str(value)
85
+
86
+
87
+ def _is_timeout(error: BaseException) -> bool:
88
+ reason = getattr(error, "reason", None)
89
+ return isinstance(error, (TimeoutError, socket.timeout)) or isinstance(reason, (TimeoutError, socket.timeout))
90
+
91
+
92
+ def _no_answer(error: BaseException) -> PlotworkApiError:
93
+ if _is_timeout(error):
94
+ return PlotworkApiError(0, "request.timeout", "The Plotwork API did not answer in time")
95
+ return PlotworkApiError(0, "request.failed", "The Plotwork API could not be reached")
96
+
97
+
98
+ def _error_of(status: int, raw: bytes, request_id: Optional[str]) -> PlotworkApiError:
99
+ try:
100
+ body = json.loads(raw.decode("utf-8"))
101
+ except ValueError:
102
+ body = None # Not JSON (a proxy's page): the status says enough.
103
+ if isinstance(body, dict) and isinstance(body.get("errorCode"), str):
104
+ message = body.get("message")
105
+ envelope_id = body.get("requestId")
106
+ return PlotworkApiError(
107
+ status,
108
+ body["errorCode"],
109
+ message if isinstance(message, str) else f"HTTP {status}",
110
+ body.get("details"),
111
+ envelope_id if isinstance(envelope_id, str) else request_id,
112
+ )
113
+ return PlotworkApiError(status, f"http.{status}", f"The Plotwork API answered HTTP {status}", None, request_id)
114
+
115
+
116
+ def _default_opener() -> Opener:
117
+ director = urllib.request.build_opener()
118
+ return director.open
119
+
120
+
121
+ class Transport:
122
+ """How the client sends requests: the address, the headers, the timeout, the retries, and the opener."""
123
+
124
+ def __init__(
125
+ self,
126
+ *,
127
+ base_url: str,
128
+ headers: Mapping[str, str],
129
+ timeout: float,
130
+ max_retries: int,
131
+ opener: Optional[Opener] = None,
132
+ sleep: Optional[Callable[[float], Any]] = None,
133
+ ) -> None:
134
+ self.base_url = base_url
135
+ self.headers = dict(headers)
136
+ self.timeout = timeout
137
+ self.max_retries = max(0, max_retries)
138
+ self.opener: Opener = opener if opener is not None else _default_opener()
139
+ self.sleep: Callable[[float], Any] = sleep if sleep is not None else time.sleep
140
+
141
+ def url_of(self, path: str, query: Optional[Mapping[str, Any]] = None) -> str:
142
+ """The address of ``path`` on the base address, with every query value that is not ``None``."""
143
+ base = self.base_url if self.base_url.endswith("/") else f"{self.base_url}/"
144
+ url = urllib.parse.urljoin(base, path)
145
+ pairs = [(name, _query_value(value)) for name, value in (query or {}).items() if value is not None]
146
+ return f"{url}?{urllib.parse.urlencode(pairs)}" if pairs else url
147
+
148
+ def request(self, method: str, path: str, *, idempotent: bool, body: Any = None, query: Optional[Mapping[str, Any]] = None) -> Any:
149
+ """Sends one request and returns the decoded JSON answer, or raises ``PlotworkApiError``."""
150
+ retries = self.max_retries if idempotent else 0
151
+ headers = {"Accept": "application/json", "User-Agent": f"plotwork-python/{__version__}"}
152
+ data = None
153
+ if body is not None:
154
+ headers["Content-Type"] = "application/json"
155
+ data = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
156
+ headers.update(self.headers)
157
+ url = self.url_of(path, query)
158
+ attempt = 0
159
+ while True:
160
+ prepared = urllib.request.Request(url, data=data, headers=headers, method=method)
161
+ try:
162
+ response = self.opener(prepared, timeout=self.timeout)
163
+ except urllib.error.HTTPError as answer:
164
+ response = answer
165
+ except (urllib.error.URLError, OSError) as error:
166
+ raise _no_answer(error) from None
167
+ try:
168
+ status = _status_of(response)
169
+ if 200 <= status < 300:
170
+ try:
171
+ raw = response.read()
172
+ except OSError as error:
173
+ raise _no_answer(error) from None
174
+ return None if status == 204 or raw == b"" else json.loads(raw.decode("utf-8"))
175
+ if status in (429, 503) and attempt < retries:
176
+ attempt += 1
177
+ self.sleep(retry_delay(_header(response, "retry-after"), attempt))
178
+ continue
179
+ try:
180
+ raw = response.read()
181
+ except OSError:
182
+ raw = b""
183
+ raise _error_of(status, raw, _header(response, "x-request-id"))
184
+ finally:
185
+ close = getattr(response, "close", None)
186
+ if close is not None:
187
+ close()
plotwork/_version.py ADDED
@@ -0,0 +1,3 @@
1
+ """The version of the ``plotwork`` package; ``pyproject.toml`` names the same one, and a test holds them equal."""
2
+
3
+ __version__ = "0.1.0"
plotwork/py.typed ADDED
File without changes
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.5
2
+ Name: plotwork
3
+ Version: 0.1.0
4
+ Summary: The Plotwork API from Python: validate, draw, render, import and publish diagrams, on the standard library alone.
5
+ Project-URL: Homepage, https://plotwork.dev
6
+ Project-URL: Documentation, https://docs.plotwork.dev/sdks
7
+ Project-URL: Source, https://github.com/plotwork-official/plotwork-sdk
8
+ Project-URL: Issues, https://github.com/plotwork-official/plotwork-sdk/issues
9
+ Author: Plotwork
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api-client,architecture,diagrams,diagrams-as-code,plotwork,svg
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: Documentation
24
+ Classifier: Topic :: Multimedia :: Graphics
25
+ Classifier: Topic :: Software Development :: Documentation
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+
30
+ <p align="center">
31
+ <a href="https://plotwork.dev">
32
+ <picture>
33
+ <source media="(prefers-color-scheme: dark)" srcset="https://plotwork.dev/brand/logo/plotwork-lockup-on-dark.svg">
34
+ <img alt="Plotwork" src="https://plotwork.dev/brand/logo/plotwork-lockup-on-light.svg" width="240">
35
+ </picture>
36
+ </a>
37
+ </p>
38
+
39
+ # plotwork
40
+
41
+ <p>
42
+ <a href="https://pypi.org/project/plotwork/"><img alt="PyPI" src="https://img.shields.io/pypi/v/plotwork?logo=pypi&logoColor=white&color=0f766e"></a>
43
+ <img alt="Python" src="https://img.shields.io/pypi/pyversions/plotwork?logo=python&logoColor=white&color=0f766e">
44
+ <a href="https://pypi.org/project/plotwork/#provenance"><img alt="Provenance" src="https://img.shields.io/badge/provenance-attested-0f766e?logo=sigstore&logoColor=white"></a>
45
+ <img alt="Runtime dependencies" src="https://img.shields.io/badge/runtime%20dependencies-0-0f766e?logo=dependabot&logoColor=white">
46
+ <img alt="Typed" src="https://img.shields.io/badge/types-generated%20from%20OpenAPI-3776ab?logo=python&logoColor=white">
47
+ </p>
48
+
49
+ The hosted Plotwork API from Python. Every `/v1` operation (validate, draw, render, import a file, publish, patterns, icons) is a typed method generated from the API's OpenAPI document, sent with `urllib` from the standard library alone. It runs on Python 3.10 and later.
50
+
51
+ ## TL;DR
52
+
53
+ | Question | Answer |
54
+ |---|---|
55
+ | What does it install? | Nothing else. |
56
+ | Where do the types come from? | The API's OpenAPI document, kept in the SDK repository as [`spec/openapi.json`](https://github.com/plotwork-official/plotwork-sdk/blob/main/spec/openapi.json). Every request and answer is a `TypedDict`. |
57
+ | How do I sign in? | An API key from the Plotwork portal (`api_key`), or an OAuth access token (`access_token`). |
58
+ | Does a mistake in a diagram raise? | No. It is part of the answer, with a line, a column and a hint. Only a failed request raises, as `PlotworkApiError` with a stable `error_code`. |
59
+ | Does it retry? | A safe call is retried twice on 429 and 503, after the API's `Retry-After`. Publishing is never retried. |
60
+ | Is there a TypeScript client? | Yes, [`@plotwork.official/sdk`](https://www.npmjs.com/package/@plotwork.official/sdk), with the same calls in camelCase. |
61
+
62
+ ## Install
63
+
64
+ ```bash
65
+ pip install plotwork
66
+ ```
67
+
68
+ ## Use
69
+
70
+ ```python
71
+ import os
72
+
73
+ from plotwork import Plotwork
74
+
75
+ plotwork = Plotwork(api_key=os.environ["PLOTWORK_API_KEY"])
76
+
77
+ source = 'flow "Checkout"\nnode web service "Web tier"\nnode api service "Orders API"\nweb -> api : "places order"\n'
78
+
79
+ checked = plotwork.diagrams.validate(source=source)
80
+ if not checked["valid"]:
81
+ for error in checked["errors"]:
82
+ print(error["line"], error["column"], error["message"], error.get("hint"))
83
+
84
+ published = plotwork.diagrams.publish(source=source, embedSource=True)
85
+ # A diagram an organisation's policy keeps private has no public link.
86
+ if "url" in published:
87
+ print(published["url"])
88
+ ```
89
+
90
+ A method takes the request's fields as keyword arguments, named as the API names them (`embedSource`, `documentPath`), and returns the answer as a plain `dict` typed by a `TypedDict`. A body you already have as a `dict` is passed with `**body`. Keep the key in an environment variable or your CI's secret store, never in a file you commit.
91
+
92
+ | Group | Calls |
93
+ |---|---|
94
+ | `diagrams` | `validate`, `draw`, `render`, `outline`, `read`, `edit`, `compare`, `suggest`, `convert_mermaid`, `publish` |
95
+ | `import_source` | A diagram from Terraform, OpenAPI, SQL, Kubernetes, Compose and the other formats the API reads |
96
+ | `markdown` | `sync`, `plan` |
97
+ | `patterns` | `find`, `get`, `draw`, `check` |
98
+ | `icons` | `search`, `map` |
99
+ | `vocabulary` | The language's statements and limits, for a kind or a topic |
100
+
101
+ ## Errors
102
+
103
+ ```python
104
+ from plotwork import PlotworkApiError
105
+
106
+ try:
107
+ plotwork.diagrams.render(source=source)
108
+ except PlotworkApiError as error:
109
+ print(error.status, error.error_code, error.request_id)
110
+ ```
111
+
112
+ | `error_code` | Means |
113
+ |---|---|
114
+ | `auth.missing`, `auth.invalid_key` | No key, or one the API does not accept |
115
+ | `rate_limit.exceeded` | Too many calls; already retried for a safe call |
116
+ | `http.<status>` | An answer that was not the API's error envelope, such as a proxy's page |
117
+ | `request.timeout`, `request.failed` | No answer came; `status` is 0 |
118
+
119
+ ## Options
120
+
121
+ | Option | Default |
122
+ |---|---|
123
+ | `api_key` | none |
124
+ | `access_token` | none |
125
+ | `base_url` | `https://api.plotwork.dev` (`https://api-preview.plotwork.dev` for preview, or a self-hosted engine) |
126
+ | `timeout` | `60` seconds |
127
+ | `max_retries` | `2` |
128
+ | `opener` | `urllib.request.build_opener().open`; pass your own for a proxy or a test |
129
+ | `sleep` | `time.sleep`, the wait before a retry |
130
+
131
+ `plotwork.with_options(timeout=120)` gives a copy with another timeout or number of retries, for a slow call such as a large import.
132
+
133
+ ## See also
134
+
135
+ | Page | Why |
136
+ |---|---|
137
+ | [SDKs guide](https://docs.plotwork.dev/sdks) | Every Plotwork SDK, and which to choose |
138
+ | [API reference](https://docs.plotwork.dev) | What each operation does |
139
+ | [TypeScript client](https://www.npmjs.com/package/@plotwork.official/sdk) | The same API from TypeScript |
140
+ | [Viewer](https://www.npmjs.com/package/@plotwork.official/viewer) | Show what you publish, interactively, in a web page |
141
+ | [Source](https://github.com/plotwork-official/plotwork-sdk/tree/main/python) | This package, its tests and its generator |
@@ -0,0 +1,9 @@
1
+ plotwork/__init__.py,sha256=eTQnRuuT5YAYs9rdw0cc8zE2RKtpskUoE4jS2mGs38M,3464
2
+ plotwork/_generated.py,sha256=B0ZaWhjwyexxDZhzg0OlSRsKRp5pC4iLHHfGqR8nvd8,114022
3
+ plotwork/_request.py,sha256=itD8pvNNioW-DYrNcSCuWapDEQGE-aU1YsLtP32dJp0,7747
4
+ plotwork/_version.py,sha256=XR92RURavhZFjFy8y6Atyjs7_paojt-E7BKhSavuEbU,138
5
+ plotwork/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ plotwork-0.1.0.dist-info/METADATA,sha256=8kAYBZlyme17saQqfHgF90Hq51rM96UUVtuKWTP1U7k,6661
7
+ plotwork-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
8
+ plotwork-0.1.0.dist-info/licenses/LICENSE,sha256=qwged3eiPcCNEOysunrXSLtz_IQpKD4xzLQlqmNEfO0,1065
9
+ plotwork-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Plotwork
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.