periplus-python-sdk 0.3.0__tar.gz → 0.5.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.
Files changed (26) hide show
  1. periplus_python_sdk-0.5.0/PKG-INFO +237 -0
  2. periplus_python_sdk-0.5.0/README.md +217 -0
  3. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/pyproject.toml +8 -1
  4. periplus_python_sdk-0.5.0/src/periplus_python_sdk.egg-info/PKG-INFO +237 -0
  5. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_python_sdk.egg-info/SOURCES.txt +6 -1
  6. periplus_python_sdk-0.5.0/src/periplus_python_sdk.egg-info/entry_points.txt +2 -0
  7. periplus_python_sdk-0.5.0/src/periplus_python_sdk.egg-info/requires.txt +9 -0
  8. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_sdk/__init__.py +2 -1
  9. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_sdk/client.py +11 -5
  10. periplus_python_sdk-0.5.0/src/periplus_sdk/dbapi.py +324 -0
  11. periplus_python_sdk-0.5.0/src/periplus_sdk/sqlalchemy.py +139 -0
  12. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_sdk/types.py +5 -0
  13. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/tests/test_client.py +28 -4
  14. periplus_python_sdk-0.5.0/tests/test_dbapi.py +118 -0
  15. periplus_python_sdk-0.5.0/tests/test_notebook.py +87 -0
  16. periplus_python_sdk-0.3.0/PKG-INFO +0 -123
  17. periplus_python_sdk-0.3.0/README.md +0 -108
  18. periplus_python_sdk-0.3.0/src/periplus_python_sdk.egg-info/PKG-INFO +0 -123
  19. periplus_python_sdk-0.3.0/src/periplus_python_sdk.egg-info/requires.txt +0 -2
  20. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/LICENSE +0 -0
  21. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/NOTICE +0 -0
  22. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/setup.cfg +0 -0
  23. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_python_sdk.egg-info/dependency_links.txt +0 -0
  24. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_python_sdk.egg-info/top_level.txt +0 -0
  25. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_sdk/errors.py +0 -0
  26. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.5.0}/src/periplus_sdk/py.typed +0 -0
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: periplus-python-sdk
3
+ Version: 0.5.0
4
+ Summary: Read-only Python client for the public Periplus query API
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Repository, https://github.com/elei-io/periplus
7
+ Project-URL: Issues, https://github.com/elei-io/periplus/issues
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ License-File: NOTICE
12
+ Requires-Dist: httpx>=0.28
13
+ Requires-Dist: pydantic<3,>=2.12
14
+ Provides-Extra: sqlalchemy
15
+ Requires-Dist: sqlalchemy<3,>=2.0; extra == "sqlalchemy"
16
+ Provides-Extra: notebook
17
+ Requires-Dist: sqlalchemy<3,>=2.0; extra == "notebook"
18
+ Requires-Dist: marimo[sql]>=0.24.1; extra == "notebook"
19
+ Dynamic: license-file
20
+
21
+ # Periplus Python SDK
22
+
23
+ A read-only client for the public Periplus query API. Python 3.11 or later.
24
+ Configure the **public web application URL**, not the internal query or control service.
25
+ No API token, DuckDB installation or lake credentials are needed.
26
+
27
+ ```python
28
+ from periplus_sdk import Client
29
+
30
+ with Client("http://localhost:8080") as client:
31
+ result = client.execute(
32
+ "SELECT capture_id FROM public_v1.capture LIMIT ?", [10]
33
+ )
34
+ print(result.columns, result.types)
35
+ print(result.rows)
36
+ print(result.source_snapshot, result.truncated)
37
+ ```
38
+
39
+ For a hosted deployment, replace the URL with its public HTTPS origin. Alternatively set
40
+ `PERIPLUS_PUBLIC_URL` and use `Client()`. An optional URL path prefix is preserved.
41
+ The client reuses HTTP connections; close it with a context manager or `close()`.
42
+
43
+ ## Marimo SQL cells and schema browser
44
+
45
+ Install the notebook integration from PyPI:
46
+
47
+ ```sh
48
+ uv add "periplus-python-sdk[notebook]>=0.5.0"
49
+ ```
50
+
51
+ In a Python setup cell, create a SQLAlchemy engine:
52
+
53
+ ```python
54
+ from sqlalchemy import create_engine
55
+
56
+ pp = create_engine(
57
+ "periplus:///public_v1",
58
+ connect_args={"base_url": "https://periplus.dev", "mode": "stable"},
59
+ )
60
+ ```
61
+
62
+ Add a SQL cell, select **pp** in its connection dropdown, and enter:
63
+
64
+ ```sql
65
+ SELECT capture_id, requested_url
66
+ FROM public_v1.capture
67
+ LIMIT 10
68
+ ```
69
+
70
+ Marimo displays the result as a table. Expand **pp → public_v1** in Data Sources
71
+ to discover views and expand a view to load its columns for SQL completion.
72
+ Discovery uses bounded `SHOW TABLES` and `DESCRIBE` through the same public API;
73
+ no internal catalogue or storage credentials are used. Truncated discovery fails
74
+ explicitly rather than displaying a silently incomplete schema. To eagerly load
75
+ schemas and views, enable their discovery in marimo's Packages & Data settings.
76
+ Column discovery is on demand by default, to avoid many public API requests.
77
+
78
+ The Python equivalent of a SQL cell is:
79
+
80
+ ```python
81
+ import marimo as mo
82
+
83
+ captures = mo.sql(
84
+ "SELECT capture_id FROM public_v1.capture LIMIT 10",
85
+ engine=pp,
86
+ )
87
+ ```
88
+
89
+ Set `mode="experimental"` in `connect_args` for the experimental service. The URL
90
+ path names the public schema; the HTTPS endpoint belongs in `base_url` (or set
91
+ `PERIPLUS_PUBLIC_URL`). Run `pp.dispose()` when finished. This is a read-only
92
+ SQLAlchemy dialect for textual SQL and reflection, not a writable ORM backend.
93
+ Each statement has its own server snapshot; SQLAlchemy transaction blocks do not
94
+ provide a shared snapshot or rollback. The adapter makes no transaction requests.
95
+
96
+ A complete notebook is in `examples/notebook.py`. The integration is tested with
97
+ marimo 0.24.1 and SQLAlchemy 2.x. For SQLAlchemy without marimo, install the
98
+ `sqlalchemy` extra instead of `notebook`.
99
+
100
+ ## DB-API connection
101
+
102
+ For SQL cells without schema browsing, or standard cursor-based Python code:
103
+
104
+ ```python
105
+ from periplus_sdk import connect
106
+
107
+ with connect("https://periplus.dev", mode="stable") as connection:
108
+ with connection.cursor() as cursor:
109
+ cursor.execute("SELECT capture_id FROM public_v1.capture LIMIT ?", [10])
110
+ print(cursor.description)
111
+ print(cursor.fetchall())
112
+ print(cursor.result.source_snapshot)
113
+ ```
114
+
115
+ Connections expose `cursor`, `execute`, `close`, and context managers. Cursors
116
+ support `execute`, `fetchone`, `fetchmany`, `fetchall`, iteration, and close.
117
+ Use positional `?` parameters. Decimal and temporal parameters are sent as
118
+ strings; use explicit SQL casts. Binary and nested parameters are not supported
119
+ by this adapter. Fetching only consumes the bounded result already received;
120
+ it never issues pagination or retries. Connections/cursors are not thread-shared.
121
+ `commit()` is a no-op; `rollback()` and `executemany()` are unsupported.
122
+
123
+ `cursor.result` preserves the original query response. `connection.last_result`
124
+ also retains it after marimo closes a cursor; a new execution clears it first.
125
+ Truncation emits `periplus_sdk.dbapi.TruncationWarning` and sets `rowcount` to -1.
126
+ DB-API failures use the standard exception hierarchy in `periplus_sdk.dbapi`;
127
+ HTTP errors retain `status_code`, `code`, and `retry_after_seconds`.
128
+
129
+ Scalar integer, floating-point, decimal, date, time, timestamp and BLOB results
130
+ are decoded to Python values. UUIDs remain strings. Nested/other SQL types keep
131
+ their JSON wire representation; out-of-range dates/timestamps remain strings.
132
+ Temporal precision is limited to what the server JSON transport preserves.
133
+ The cursor preserves duplicate column names, but dataframe libraries/marimo may
134
+ not: use unique SQL aliases. Dataframe inference can lose types for empty or
135
+ all-null results; `cursor.description` retains the SQL type names.
136
+
137
+ ## Stable and experimental APIs
138
+
139
+ Both clients accept `mode="stable"` (the default) or `mode="experimental"` at initialization:
140
+
141
+ ```python
142
+ with Client("https://periplus.dev", mode="experimental") as client:
143
+ result = client.execute("SELECT capture_id FROM public_v1.capture LIMIT 1")
144
+ print(result.query_mode, result.compiler_version, result.optimizations)
145
+ ```
146
+
147
+ The selected mode applies to preparation, execution, and helper discovery. Experimental
148
+ requests use the public application's `/api/query/experimental/` routes. There is no
149
+ automatic fallback to stable if the experimental service is unavailable.
150
+ `AsyncClient` accepts the same option. Invalid modes raise `ConfigurationError`.
151
+
152
+ ## Preparation and helpers
153
+
154
+ ```python
155
+ with Client("http://localhost:8080") as client:
156
+ prepared = client.prepare("SELECT capture_id FROM public_v1.capture LIMIT ?", [10])
157
+ print(prepared.diagnostics, prepared.plan)
158
+ result = client.execute(prepared.sql, prepared.parameters)
159
+ helpers = client.helpers()
160
+ print(helpers.catalogue_version, helpers.helpers)
161
+ ```
162
+
163
+ Preparation validates and explains without executing the analytical query. Execution independently
164
+ validates and prepares; a prior preparation never authorizes SQL. Linting, diagnostics and future
165
+ SQL optimizations belong to the server. The SDK sends SQL unchanged.
166
+
167
+ ## Async use
168
+
169
+ ```python
170
+ from periplus_sdk import AsyncClient
171
+
172
+ async def observations():
173
+ async with AsyncClient("http://localhost:8080") as client:
174
+ return await client.execute("SELECT capture_id FROM public_v1.capture LIMIT 10")
175
+ ```
176
+
177
+ Use `aclose()` when managing an async client's lifetime explicitly.
178
+
179
+ ## Permissions, results and errors
180
+
181
+ - The same public SQL feature switch, shared rate budget, namespace validation and read-only
182
+ execution apply as in the public web workspace. The SDK provides no writes, crawling,
183
+ administrative controls or direct lake attachment.
184
+ - Results retain `query_id`, SQL, parameters, diagnostics, plan, columns, SQL types, JSON rows,
185
+ elapsed milliseconds, `source_snapshot` and `truncated`. Decimals and large integers remain
186
+ strings exactly as returned by the server. Duplicate column names are preserved.
187
+ - Operator-configured execution limits default to 1,000 rows, an 8 MiB result budget and a
188
+ 20-second server deadline. Always inspect `truncated`. The SDK does not silently fetch more rows or retry.
189
+ - `ApiError` exposes `status_code`, safe `code`, and `retry_after_seconds` when supplied.
190
+ `TransportError` means HTTP failed; `ResponseError` means a malformed successful response.
191
+ The client timeout defaults to 140 seconds and can be set with `timeout=`. A timeout or local
192
+ cancellation does not guarantee server cancellation. Redirects are not followed automatically.
193
+ - Preparation and execution are attributed to `sdk` in the existing private query history.
194
+ Original SQL and parameters are retained for 30 days; result rows are not stored. Recording is
195
+ best-effort and can be lost during outages or backpressure. This label is not a user identity.
196
+
197
+ ## Installation and verification
198
+
199
+ Install the public-v1 client from PyPI:
200
+
201
+ ```sh
202
+ python -m pip install "periplus-python-sdk>=0.5.0"
203
+ ```
204
+
205
+ Version 0.5.0 supports the current public-v1 contract. For production, configure
206
+ `PERIPLUS_PUBLIC_URL=https://periplus.dev`; no API token is required.
207
+ Run the installed package against an available public app:
208
+
209
+ ```sh
210
+ PERIPLUS_PUBLIC_URL=http://localhost:8080 python packages/periplus-python-sdk/examples/smoke.py
211
+ ```
212
+
213
+ ## Releasing
214
+
215
+ Repository CI publishes immutable releases from tags named
216
+ `periplus-python-sdk-v<version>`. The tag must exactly match the static version
217
+ in `pyproject.toml`; for example, version `0.5.0` is released with:
218
+
219
+ ```sh
220
+ git tag periplus-python-sdk-v0.5.0
221
+ git push origin periplus-python-sdk-v0.5.0
222
+ ```
223
+
224
+ PyPI publishing uses Trusted Publishing rather than a stored API token. The
225
+ PyPI publisher must be configured for GitHub owner `elei-io`, repository
226
+ `periplus`, workflow `python-sdk-release.yml`, and environment `pypi`. Protect
227
+ that GitHub environment with required reviewers before the first release.
228
+
229
+ ## Public v1
230
+
231
+ Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.5.0"`. The previously published 0.2.0 release predates this contract. `prepare` and `execute` accept keyword-only `schema_version="public_v1"` (the default); responses preserve `schema_version` separately from `source_snapshot`. Unavailable versions are rejected by the server.
232
+
233
+ ## License
234
+
235
+ Copyright (c) 2026 Ekku Leivonen (elei.io). Licensed under [Apache-2.0](LICENSE);
236
+ see [NOTICE](NOTICE). The server and other repository packages have separate
237
+ licensing described in the root LICENSING.md.
@@ -0,0 +1,217 @@
1
+ # Periplus Python SDK
2
+
3
+ A read-only client for the public Periplus query API. Python 3.11 or later.
4
+ Configure the **public web application URL**, not the internal query or control service.
5
+ No API token, DuckDB installation or lake credentials are needed.
6
+
7
+ ```python
8
+ from periplus_sdk import Client
9
+
10
+ with Client("http://localhost:8080") as client:
11
+ result = client.execute(
12
+ "SELECT capture_id FROM public_v1.capture LIMIT ?", [10]
13
+ )
14
+ print(result.columns, result.types)
15
+ print(result.rows)
16
+ print(result.source_snapshot, result.truncated)
17
+ ```
18
+
19
+ For a hosted deployment, replace the URL with its public HTTPS origin. Alternatively set
20
+ `PERIPLUS_PUBLIC_URL` and use `Client()`. An optional URL path prefix is preserved.
21
+ The client reuses HTTP connections; close it with a context manager or `close()`.
22
+
23
+ ## Marimo SQL cells and schema browser
24
+
25
+ Install the notebook integration from PyPI:
26
+
27
+ ```sh
28
+ uv add "periplus-python-sdk[notebook]>=0.5.0"
29
+ ```
30
+
31
+ In a Python setup cell, create a SQLAlchemy engine:
32
+
33
+ ```python
34
+ from sqlalchemy import create_engine
35
+
36
+ pp = create_engine(
37
+ "periplus:///public_v1",
38
+ connect_args={"base_url": "https://periplus.dev", "mode": "stable"},
39
+ )
40
+ ```
41
+
42
+ Add a SQL cell, select **pp** in its connection dropdown, and enter:
43
+
44
+ ```sql
45
+ SELECT capture_id, requested_url
46
+ FROM public_v1.capture
47
+ LIMIT 10
48
+ ```
49
+
50
+ Marimo displays the result as a table. Expand **pp → public_v1** in Data Sources
51
+ to discover views and expand a view to load its columns for SQL completion.
52
+ Discovery uses bounded `SHOW TABLES` and `DESCRIBE` through the same public API;
53
+ no internal catalogue or storage credentials are used. Truncated discovery fails
54
+ explicitly rather than displaying a silently incomplete schema. To eagerly load
55
+ schemas and views, enable their discovery in marimo's Packages & Data settings.
56
+ Column discovery is on demand by default, to avoid many public API requests.
57
+
58
+ The Python equivalent of a SQL cell is:
59
+
60
+ ```python
61
+ import marimo as mo
62
+
63
+ captures = mo.sql(
64
+ "SELECT capture_id FROM public_v1.capture LIMIT 10",
65
+ engine=pp,
66
+ )
67
+ ```
68
+
69
+ Set `mode="experimental"` in `connect_args` for the experimental service. The URL
70
+ path names the public schema; the HTTPS endpoint belongs in `base_url` (or set
71
+ `PERIPLUS_PUBLIC_URL`). Run `pp.dispose()` when finished. This is a read-only
72
+ SQLAlchemy dialect for textual SQL and reflection, not a writable ORM backend.
73
+ Each statement has its own server snapshot; SQLAlchemy transaction blocks do not
74
+ provide a shared snapshot or rollback. The adapter makes no transaction requests.
75
+
76
+ A complete notebook is in `examples/notebook.py`. The integration is tested with
77
+ marimo 0.24.1 and SQLAlchemy 2.x. For SQLAlchemy without marimo, install the
78
+ `sqlalchemy` extra instead of `notebook`.
79
+
80
+ ## DB-API connection
81
+
82
+ For SQL cells without schema browsing, or standard cursor-based Python code:
83
+
84
+ ```python
85
+ from periplus_sdk import connect
86
+
87
+ with connect("https://periplus.dev", mode="stable") as connection:
88
+ with connection.cursor() as cursor:
89
+ cursor.execute("SELECT capture_id FROM public_v1.capture LIMIT ?", [10])
90
+ print(cursor.description)
91
+ print(cursor.fetchall())
92
+ print(cursor.result.source_snapshot)
93
+ ```
94
+
95
+ Connections expose `cursor`, `execute`, `close`, and context managers. Cursors
96
+ support `execute`, `fetchone`, `fetchmany`, `fetchall`, iteration, and close.
97
+ Use positional `?` parameters. Decimal and temporal parameters are sent as
98
+ strings; use explicit SQL casts. Binary and nested parameters are not supported
99
+ by this adapter. Fetching only consumes the bounded result already received;
100
+ it never issues pagination or retries. Connections/cursors are not thread-shared.
101
+ `commit()` is a no-op; `rollback()` and `executemany()` are unsupported.
102
+
103
+ `cursor.result` preserves the original query response. `connection.last_result`
104
+ also retains it after marimo closes a cursor; a new execution clears it first.
105
+ Truncation emits `periplus_sdk.dbapi.TruncationWarning` and sets `rowcount` to -1.
106
+ DB-API failures use the standard exception hierarchy in `periplus_sdk.dbapi`;
107
+ HTTP errors retain `status_code`, `code`, and `retry_after_seconds`.
108
+
109
+ Scalar integer, floating-point, decimal, date, time, timestamp and BLOB results
110
+ are decoded to Python values. UUIDs remain strings. Nested/other SQL types keep
111
+ their JSON wire representation; out-of-range dates/timestamps remain strings.
112
+ Temporal precision is limited to what the server JSON transport preserves.
113
+ The cursor preserves duplicate column names, but dataframe libraries/marimo may
114
+ not: use unique SQL aliases. Dataframe inference can lose types for empty or
115
+ all-null results; `cursor.description` retains the SQL type names.
116
+
117
+ ## Stable and experimental APIs
118
+
119
+ Both clients accept `mode="stable"` (the default) or `mode="experimental"` at initialization:
120
+
121
+ ```python
122
+ with Client("https://periplus.dev", mode="experimental") as client:
123
+ result = client.execute("SELECT capture_id FROM public_v1.capture LIMIT 1")
124
+ print(result.query_mode, result.compiler_version, result.optimizations)
125
+ ```
126
+
127
+ The selected mode applies to preparation, execution, and helper discovery. Experimental
128
+ requests use the public application's `/api/query/experimental/` routes. There is no
129
+ automatic fallback to stable if the experimental service is unavailable.
130
+ `AsyncClient` accepts the same option. Invalid modes raise `ConfigurationError`.
131
+
132
+ ## Preparation and helpers
133
+
134
+ ```python
135
+ with Client("http://localhost:8080") as client:
136
+ prepared = client.prepare("SELECT capture_id FROM public_v1.capture LIMIT ?", [10])
137
+ print(prepared.diagnostics, prepared.plan)
138
+ result = client.execute(prepared.sql, prepared.parameters)
139
+ helpers = client.helpers()
140
+ print(helpers.catalogue_version, helpers.helpers)
141
+ ```
142
+
143
+ Preparation validates and explains without executing the analytical query. Execution independently
144
+ validates and prepares; a prior preparation never authorizes SQL. Linting, diagnostics and future
145
+ SQL optimizations belong to the server. The SDK sends SQL unchanged.
146
+
147
+ ## Async use
148
+
149
+ ```python
150
+ from periplus_sdk import AsyncClient
151
+
152
+ async def observations():
153
+ async with AsyncClient("http://localhost:8080") as client:
154
+ return await client.execute("SELECT capture_id FROM public_v1.capture LIMIT 10")
155
+ ```
156
+
157
+ Use `aclose()` when managing an async client's lifetime explicitly.
158
+
159
+ ## Permissions, results and errors
160
+
161
+ - The same public SQL feature switch, shared rate budget, namespace validation and read-only
162
+ execution apply as in the public web workspace. The SDK provides no writes, crawling,
163
+ administrative controls or direct lake attachment.
164
+ - Results retain `query_id`, SQL, parameters, diagnostics, plan, columns, SQL types, JSON rows,
165
+ elapsed milliseconds, `source_snapshot` and `truncated`. Decimals and large integers remain
166
+ strings exactly as returned by the server. Duplicate column names are preserved.
167
+ - Operator-configured execution limits default to 1,000 rows, an 8 MiB result budget and a
168
+ 20-second server deadline. Always inspect `truncated`. The SDK does not silently fetch more rows or retry.
169
+ - `ApiError` exposes `status_code`, safe `code`, and `retry_after_seconds` when supplied.
170
+ `TransportError` means HTTP failed; `ResponseError` means a malformed successful response.
171
+ The client timeout defaults to 140 seconds and can be set with `timeout=`. A timeout or local
172
+ cancellation does not guarantee server cancellation. Redirects are not followed automatically.
173
+ - Preparation and execution are attributed to `sdk` in the existing private query history.
174
+ Original SQL and parameters are retained for 30 days; result rows are not stored. Recording is
175
+ best-effort and can be lost during outages or backpressure. This label is not a user identity.
176
+
177
+ ## Installation and verification
178
+
179
+ Install the public-v1 client from PyPI:
180
+
181
+ ```sh
182
+ python -m pip install "periplus-python-sdk>=0.5.0"
183
+ ```
184
+
185
+ Version 0.5.0 supports the current public-v1 contract. For production, configure
186
+ `PERIPLUS_PUBLIC_URL=https://periplus.dev`; no API token is required.
187
+ Run the installed package against an available public app:
188
+
189
+ ```sh
190
+ PERIPLUS_PUBLIC_URL=http://localhost:8080 python packages/periplus-python-sdk/examples/smoke.py
191
+ ```
192
+
193
+ ## Releasing
194
+
195
+ Repository CI publishes immutable releases from tags named
196
+ `periplus-python-sdk-v<version>`. The tag must exactly match the static version
197
+ in `pyproject.toml`; for example, version `0.5.0` is released with:
198
+
199
+ ```sh
200
+ git tag periplus-python-sdk-v0.5.0
201
+ git push origin periplus-python-sdk-v0.5.0
202
+ ```
203
+
204
+ PyPI publishing uses Trusted Publishing rather than a stored API token. The
205
+ PyPI publisher must be configured for GitHub owner `elei-io`, repository
206
+ `periplus`, workflow `python-sdk-release.yml`, and environment `pypi`. Protect
207
+ that GitHub environment with required reviewers before the first release.
208
+
209
+ ## Public v1
210
+
211
+ Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.5.0"`. The previously published 0.2.0 release predates this contract. `prepare` and `execute` accept keyword-only `schema_version="public_v1"` (the default); responses preserve `schema_version` separately from `source_snapshot`. Unavailable versions are rejected by the server.
212
+
213
+ ## License
214
+
215
+ Copyright (c) 2026 Ekku Leivonen (elei.io). Licensed under [Apache-2.0](LICENSE);
216
+ see [NOTICE](NOTICE). The server and other repository packages have separate
217
+ licensing described in the root LICENSING.md.
@@ -2,7 +2,7 @@
2
2
  license = "Apache-2.0"
3
3
  license-files = ["LICENSE", "NOTICE"]
4
4
  name = "periplus-python-sdk"
5
- version = "0.3.0"
5
+ version = "0.5.0"
6
6
  description = "Read-only Python client for the public Periplus query API"
7
7
  readme = "README.md"
8
8
  requires-python = ">=3.11"
@@ -11,6 +11,13 @@ dependencies = [
11
11
  "pydantic>=2.12,<3",
12
12
  ]
13
13
 
14
+ [project.optional-dependencies]
15
+ sqlalchemy = ["sqlalchemy>=2.0,<3"]
16
+ notebook = ["sqlalchemy>=2.0,<3", "marimo[sql]>=0.24.1"]
17
+
18
+ [project.entry-points."sqlalchemy.dialects"]
19
+ periplus = "periplus_sdk.sqlalchemy:PeriplusDialect"
20
+
14
21
  [project.urls]
15
22
  Repository = "https://github.com/elei-io/periplus"
16
23
  Issues = "https://github.com/elei-io/periplus/issues"