periplus-python-sdk 0.3.0__tar.gz → 0.4.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 (17) hide show
  1. {periplus_python_sdk-0.3.0/src/periplus_python_sdk.egg-info → periplus_python_sdk-0.4.0}/PKG-INFO +22 -7
  2. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/README.md +21 -6
  3. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/pyproject.toml +1 -1
  4. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0/src/periplus_python_sdk.egg-info}/PKG-INFO +22 -7
  5. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_sdk/client.py +11 -5
  6. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_sdk/types.py +5 -0
  7. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/tests/test_client.py +28 -4
  8. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/LICENSE +0 -0
  9. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/NOTICE +0 -0
  10. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/setup.cfg +0 -0
  11. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_python_sdk.egg-info/SOURCES.txt +0 -0
  12. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_python_sdk.egg-info/dependency_links.txt +0 -0
  13. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_python_sdk.egg-info/requires.txt +0 -0
  14. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_python_sdk.egg-info/top_level.txt +0 -0
  15. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_sdk/__init__.py +0 -0
  16. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_sdk/errors.py +0 -0
  17. {periplus_python_sdk-0.3.0 → periplus_python_sdk-0.4.0}/src/periplus_sdk/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: periplus-python-sdk
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Read-only Python client for the public Periplus query API
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/elei-io/periplus
@@ -35,6 +35,21 @@ For a hosted deployment, replace the URL with its public HTTPS origin. Alternati
35
35
  `PERIPLUS_PUBLIC_URL` and use `Client()`. An optional URL path prefix is preserved.
36
36
  The client reuses HTTP connections; close it with a context manager or `close()`.
37
37
 
38
+ ## Stable and experimental APIs
39
+
40
+ Both clients accept `mode="stable"` (the default) or `mode="experimental"` at initialization:
41
+
42
+ ```python
43
+ with Client("https://periplus.dev", mode="experimental") as client:
44
+ result = client.execute("SELECT capture_id FROM public_v1.capture LIMIT 1")
45
+ print(result.query_mode, result.compiler_version, result.optimizations)
46
+ ```
47
+
48
+ The selected mode applies to preparation, execution, and helper discovery. Experimental
49
+ requests use the public application's `/api/query/experimental/` routes. There is no
50
+ automatic fallback to stable if the experimental service is unavailable.
51
+ `AsyncClient` accepts the same option. Invalid modes raise `ConfigurationError`.
52
+
38
53
  ## Preparation and helpers
39
54
 
40
55
  ```python
@@ -85,10 +100,10 @@ Use `aclose()` when managing an async client's lifetime explicitly.
85
100
  Install the public-v1 client from PyPI:
86
101
 
87
102
  ```sh
88
- python -m pip install "periplus-python-sdk>=0.3.0"
103
+ python -m pip install "periplus-python-sdk>=0.4.0"
89
104
  ```
90
105
 
91
- Version 0.3.0 supports the current public-v1 contract. For production, configure
106
+ Version 0.4.0 supports the current public-v1 contract. For production, configure
92
107
  `PERIPLUS_PUBLIC_URL=https://periplus.dev`; no API token is required.
93
108
  Run the installed package against an available public app:
94
109
 
@@ -100,11 +115,11 @@ PERIPLUS_PUBLIC_URL=http://localhost:8080 python packages/periplus-python-sdk/ex
100
115
 
101
116
  Repository CI publishes immutable releases from tags named
102
117
  `periplus-python-sdk-v<version>`. The tag must exactly match the static version
103
- in `pyproject.toml`; for example, version `0.3.0` is released with:
118
+ in `pyproject.toml`; for example, version `0.4.0` is released with:
104
119
 
105
120
  ```sh
106
- git tag periplus-python-sdk-v0.3.0
107
- git push origin periplus-python-sdk-v0.3.0
121
+ git tag periplus-python-sdk-v0.4.0
122
+ git push origin periplus-python-sdk-v0.4.0
108
123
  ```
109
124
 
110
125
  PyPI publishing uses Trusted Publishing rather than a stored API token. The
@@ -114,7 +129,7 @@ that GitHub environment with required reviewers before the first release.
114
129
 
115
130
  ## Public v1
116
131
 
117
- Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.3.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.
132
+ Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.4.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.
118
133
 
119
134
  ## License
120
135
 
@@ -20,6 +20,21 @@ For a hosted deployment, replace the URL with its public HTTPS origin. Alternati
20
20
  `PERIPLUS_PUBLIC_URL` and use `Client()`. An optional URL path prefix is preserved.
21
21
  The client reuses HTTP connections; close it with a context manager or `close()`.
22
22
 
23
+ ## Stable and experimental APIs
24
+
25
+ Both clients accept `mode="stable"` (the default) or `mode="experimental"` at initialization:
26
+
27
+ ```python
28
+ with Client("https://periplus.dev", mode="experimental") as client:
29
+ result = client.execute("SELECT capture_id FROM public_v1.capture LIMIT 1")
30
+ print(result.query_mode, result.compiler_version, result.optimizations)
31
+ ```
32
+
33
+ The selected mode applies to preparation, execution, and helper discovery. Experimental
34
+ requests use the public application's `/api/query/experimental/` routes. There is no
35
+ automatic fallback to stable if the experimental service is unavailable.
36
+ `AsyncClient` accepts the same option. Invalid modes raise `ConfigurationError`.
37
+
23
38
  ## Preparation and helpers
24
39
 
25
40
  ```python
@@ -70,10 +85,10 @@ Use `aclose()` when managing an async client's lifetime explicitly.
70
85
  Install the public-v1 client from PyPI:
71
86
 
72
87
  ```sh
73
- python -m pip install "periplus-python-sdk>=0.3.0"
88
+ python -m pip install "periplus-python-sdk>=0.4.0"
74
89
  ```
75
90
 
76
- Version 0.3.0 supports the current public-v1 contract. For production, configure
91
+ Version 0.4.0 supports the current public-v1 contract. For production, configure
77
92
  `PERIPLUS_PUBLIC_URL=https://periplus.dev`; no API token is required.
78
93
  Run the installed package against an available public app:
79
94
 
@@ -85,11 +100,11 @@ PERIPLUS_PUBLIC_URL=http://localhost:8080 python packages/periplus-python-sdk/ex
85
100
 
86
101
  Repository CI publishes immutable releases from tags named
87
102
  `periplus-python-sdk-v<version>`. The tag must exactly match the static version
88
- in `pyproject.toml`; for example, version `0.3.0` is released with:
103
+ in `pyproject.toml`; for example, version `0.4.0` is released with:
89
104
 
90
105
  ```sh
91
- git tag periplus-python-sdk-v0.3.0
92
- git push origin periplus-python-sdk-v0.3.0
106
+ git tag periplus-python-sdk-v0.4.0
107
+ git push origin periplus-python-sdk-v0.4.0
93
108
  ```
94
109
 
95
110
  PyPI publishing uses Trusted Publishing rather than a stored API token. The
@@ -99,7 +114,7 @@ that GitHub environment with required reviewers before the first release.
99
114
 
100
115
  ## Public v1
101
116
 
102
- Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.3.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.
117
+ Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.4.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.
103
118
 
104
119
  ## License
105
120
 
@@ -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.4.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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: periplus-python-sdk
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Read-only Python client for the public Periplus query API
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: Repository, https://github.com/elei-io/periplus
@@ -35,6 +35,21 @@ For a hosted deployment, replace the URL with its public HTTPS origin. Alternati
35
35
  `PERIPLUS_PUBLIC_URL` and use `Client()`. An optional URL path prefix is preserved.
36
36
  The client reuses HTTP connections; close it with a context manager or `close()`.
37
37
 
38
+ ## Stable and experimental APIs
39
+
40
+ Both clients accept `mode="stable"` (the default) or `mode="experimental"` at initialization:
41
+
42
+ ```python
43
+ with Client("https://periplus.dev", mode="experimental") as client:
44
+ result = client.execute("SELECT capture_id FROM public_v1.capture LIMIT 1")
45
+ print(result.query_mode, result.compiler_version, result.optimizations)
46
+ ```
47
+
48
+ The selected mode applies to preparation, execution, and helper discovery. Experimental
49
+ requests use the public application's `/api/query/experimental/` routes. There is no
50
+ automatic fallback to stable if the experimental service is unavailable.
51
+ `AsyncClient` accepts the same option. Invalid modes raise `ConfigurationError`.
52
+
38
53
  ## Preparation and helpers
39
54
 
40
55
  ```python
@@ -85,10 +100,10 @@ Use `aclose()` when managing an async client's lifetime explicitly.
85
100
  Install the public-v1 client from PyPI:
86
101
 
87
102
  ```sh
88
- python -m pip install "periplus-python-sdk>=0.3.0"
103
+ python -m pip install "periplus-python-sdk>=0.4.0"
89
104
  ```
90
105
 
91
- Version 0.3.0 supports the current public-v1 contract. For production, configure
106
+ Version 0.4.0 supports the current public-v1 contract. For production, configure
92
107
  `PERIPLUS_PUBLIC_URL=https://periplus.dev`; no API token is required.
93
108
  Run the installed package against an available public app:
94
109
 
@@ -100,11 +115,11 @@ PERIPLUS_PUBLIC_URL=http://localhost:8080 python packages/periplus-python-sdk/ex
100
115
 
101
116
  Repository CI publishes immutable releases from tags named
102
117
  `periplus-python-sdk-v<version>`. The tag must exactly match the static version
103
- in `pyproject.toml`; for example, version `0.3.0` is released with:
118
+ in `pyproject.toml`; for example, version `0.4.0` is released with:
104
119
 
105
120
  ```sh
106
- git tag periplus-python-sdk-v0.3.0
107
- git push origin periplus-python-sdk-v0.3.0
121
+ git tag periplus-python-sdk-v0.4.0
122
+ git push origin periplus-python-sdk-v0.4.0
108
123
  ```
109
124
 
110
125
  PyPI publishing uses Trusted Publishing rather than a stored API token. The
@@ -114,7 +129,7 @@ that GitHub environment with required reviewers before the first release.
114
129
 
115
130
  ## Public v1
116
131
 
117
- Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.3.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.
132
+ Install the updated SDK from PyPI with `python -m pip install "periplus-python-sdk>=0.4.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.
118
133
 
119
134
  ## License
120
135
 
@@ -6,7 +6,7 @@ from datetime import UTC, datetime
6
6
  from email.utils import parsedate_to_datetime
7
7
  import math
8
8
  import os
9
- from typing import TypeVar
9
+ from typing import Literal, TypeVar
10
10
 
11
11
  import httpx
12
12
  from pydantic import BaseModel, JsonValue, ValidationError
@@ -79,7 +79,10 @@ def _payload(sql: str, parameters: Sequence[JsonValue] | None, schema_version: s
79
79
  class Client:
80
80
  """Reusable synchronous public query client. Close it or use a with block."""
81
81
 
82
- def __init__(self, base_url: str | None = None, *, timeout: float = 140):
82
+ def __init__(self, base_url: str | None = None, *, timeout: float = 140, mode: Literal["stable", "experimental"] = "stable"):
83
+ if mode not in {"stable", "experimental"}:
84
+ raise ConfigurationError("mode must be stable or experimental.")
85
+ self._query_path = "api/query/experimental/" if mode == "experimental" else "api/query/"
83
86
  self._http = httpx.Client(**_options(base_url, timeout))
84
87
 
85
88
  def __enter__(self) -> Client:
@@ -93,7 +96,7 @@ class Client:
93
96
 
94
97
  def _request(self, method: str, path: str, model: type[Model], **kwargs) -> Model:
95
98
  try:
96
- response = self._http.request(method, "api/query/" + path, **kwargs)
99
+ response = self._http.request(method, self._query_path + path, **kwargs)
97
100
  except httpx.RequestError:
98
101
  raise TransportError("Could not complete the public query request.") from None
99
102
  return _decode(response, model)
@@ -111,7 +114,10 @@ class Client:
111
114
  class AsyncClient:
112
115
  """Reusable asynchronous public query client. Use an async with block."""
113
116
 
114
- def __init__(self, base_url: str | None = None, *, timeout: float = 140):
117
+ def __init__(self, base_url: str | None = None, *, timeout: float = 140, mode: Literal["stable", "experimental"] = "stable"):
118
+ if mode not in {"stable", "experimental"}:
119
+ raise ConfigurationError("mode must be stable or experimental.")
120
+ self._query_path = "api/query/experimental/" if mode == "experimental" else "api/query/"
115
121
  self._http = httpx.AsyncClient(**_options(base_url, timeout))
116
122
 
117
123
  async def __aenter__(self) -> AsyncClient:
@@ -125,7 +131,7 @@ class AsyncClient:
125
131
 
126
132
  async def _request(self, method: str, path: str, model: type[Model], **kwargs) -> Model:
127
133
  try:
128
- response = await self._http.request(method, "api/query/" + path, **kwargs)
134
+ response = await self._http.request(method, self._query_path + path, **kwargs)
129
135
  except httpx.RequestError:
130
136
  raise TransportError("Could not complete the public query request.") from None
131
137
  return _decode(response, model)
@@ -1,4 +1,6 @@
1
1
  """Public query wire types; SQL types and JSON values are preserved."""
2
+ from typing import Literal
3
+
2
4
  from pydantic import BaseModel, Field, JsonValue
3
5
 
4
6
 
@@ -9,6 +11,9 @@ class Diagnostic(BaseModel):
9
11
 
10
12
 
11
13
  class PreparedQuery(BaseModel):
14
+ query_mode: Literal["stable", "experimental"]
15
+ compiler_version: str
16
+ optimizations: list[str]
12
17
  schema_version: str
13
18
  query_id: str
14
19
  sql: str
@@ -7,17 +7,17 @@ import httpx
7
7
 
8
8
  from periplus_sdk import AsyncClient, Client, ApiError, ConfigurationError, ResponseError, TransportError
9
9
 
10
- PREP = dict(schema_version='public_v1', query_id='q', sql='SELECT ? AS n', parameters=[1], diagnostics=[], plan='plan')
10
+ PREP = dict(query_mode='stable', compiler_version='public-query-v8:stable', optimizations=[], schema_version='public_v1', query_id='q', sql='SELECT ? AS n', parameters=[1], diagnostics=[], plan='plan')
11
11
  RESULT = dict(**PREP, columns=['n', 'n'], types=['BIGINT', 'DECIMAL(20,2)'],
12
12
  rows=[['9007199254740993', '123.45']], truncated=True, elapsed_ms=1.2, source_snapshot=4)
13
13
 
14
14
 
15
15
  class ClientTests(unittest.TestCase):
16
- def client(self, handler):
16
+ def client(self, handler, **options):
17
17
  factory = httpx.Client
18
18
  with patch('periplus_sdk.client.httpx.Client', side_effect=lambda **kw:
19
19
  factory(**kw, transport=httpx.MockTransport(handler))):
20
- client = Client('https://public.example/prefix/')
20
+ client = Client('https://public.example/prefix/', **options)
21
21
  self.addCleanup(client.close)
22
22
  return client
23
23
 
@@ -42,6 +42,29 @@ class ClientTests(unittest.TestCase):
42
42
  self.assertEqual(client.helpers().catalogue_version, '1.0.0')
43
43
  self.assertEqual([r.url.path for r in requests], ['/prefix/api/query/prep', '/prefix/api/query/exec', '/prefix/api/query/helpers'])
44
44
 
45
+ def test_experimental_routes_and_metadata(self):
46
+ paths = []
47
+ def handler(request):
48
+ paths.append(request.url.path)
49
+ if request.url.path.endswith('helpers'):
50
+ return httpx.Response(200, json={'catalogue_version': '1.0.0', 'helpers': []})
51
+ payload = dict(RESULT if request.url.path.endswith('exec') else PREP,
52
+ query_mode='experimental', compiler_version='public-query-v8:experimental',
53
+ optimizations=['content_scope'])
54
+ return httpx.Response(200, json=payload)
55
+ with self.client(handler, mode='experimental') as client:
56
+ prepared = client.prepare('SELECT 1')
57
+ self.assertEqual(prepared.query_mode, 'experimental')
58
+ self.assertEqual(prepared.compiler_version, 'public-query-v8:experimental')
59
+ self.assertEqual(client.execute('SELECT 1').optimizations, ['content_scope'])
60
+ client.helpers()
61
+ self.assertEqual(paths, ['/prefix/api/query/experimental/' + p for p in ['prep', 'exec', 'helpers']])
62
+
63
+ def test_invalid_mode(self):
64
+ for factory in (Client, AsyncClient):
65
+ with self.assertRaises(ConfigurationError):
66
+ factory('https://public.example', mode='unknown')
67
+
45
68
  def test_errors_preserve_categories_and_never_retry(self):
46
69
  for status, body, code in [(429, {'code': 'service_busy', 'detail': 'Busy'}, 'service_busy'),
47
70
  (403, {'code': 'feature_disabled', 'detail': 'Disabled'}, 'feature_disabled'),
@@ -101,10 +124,11 @@ class AsyncClientTests(unittest.IsolatedAsyncioTestCase):
101
124
  return httpx.Response(200, json=RESULT if request.url.path.endswith('exec') else PREP)
102
125
  with patch('periplus_sdk.client.httpx.AsyncClient', side_effect=lambda **kw:
103
126
  factory(**kw, transport=httpx.MockTransport(handler))):
104
- async with AsyncClient('https://public.example') as client:
127
+ async with AsyncClient('https://public.example/prefix/', mode='experimental') as client:
105
128
  self.assertEqual((await client.prepare('SELECT ?', [1])).parameters, [1])
106
129
  self.assertEqual((await client.execute('SELECT ?', [1])).rows, RESULT['rows'])
107
130
  self.assertEqual((await client.helpers()).catalogue_version, '1.0.0')
108
131
  self.assertTrue(client._http.is_closed)
109
132
  self.assertEqual(len(calls), 3)
133
+ self.assertEqual([r.url.path for r in calls], ['/prefix/api/query/experimental/' + p for p in ['prep', 'exec', 'helpers']])
110
134
  self.assertTrue(all(r.headers['x-periplus-query-source'] == 'sdk' for r in calls))