oceanum-prax-sdk 0.3.2__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.
- oceanum_prax_sdk-0.3.2/.github/scripts/check_pypi_release.py +198 -0
- oceanum_prax_sdk-0.3.2/.github/workflows/publish.yml +62 -0
- oceanum_prax_sdk-0.3.2/LICENSE +21 -0
- oceanum_prax_sdk-0.3.2/PKG-INFO +126 -0
- oceanum_prax_sdk-0.3.2/README.md +98 -0
- oceanum_prax_sdk-0.3.2/SOURCE_SNAPSHOT.json +60 -0
- oceanum_prax_sdk-0.3.2/pyproject.toml +120 -0
- oceanum_prax_sdk-0.3.2/src/prax/__init__.py +19 -0
- oceanum_prax_sdk-0.3.2/src/prax/_version.py +17 -0
- oceanum_prax_sdk-0.3.2/src/prax/api.py +570 -0
- oceanum_prax_sdk-0.3.2/src/prax/files/__init__.py +3 -0
- oceanum_prax_sdk-0.3.2/src/prax/files/base.py +57 -0
- oceanum_prax_sdk-0.3.2/src/prax/models/__init__.py +996 -0
- oceanum_prax_sdk-0.3.2/src/prax/project_spec_workflow.py +181 -0
- oceanum_prax_sdk-0.3.2/src/prax/py.typed +0 -0
- oceanum_prax_sdk-0.3.2/src/prax/settings.py +11 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/__init__.py +10 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/_base_types.py +18 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/_workflow.py +9 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/pipeline.py +207 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/resources.py +240 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/secret_refs.py +135 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/source_refs.py +215 -0
- oceanum_prax_sdk-0.3.2/src/prax/spec/stage.py +305 -0
- oceanum_prax_sdk-0.3.2/src/prax/utils.py +70 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate exported release provenance and plan an immutable PyPI upload."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
import tomllib
|
|
15
|
+
import urllib.error
|
|
16
|
+
import urllib.request
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
18
|
+
|
|
19
|
+
PYPI_JSON = "https://pypi.org/pypi/{package}/{version}/json"
|
|
20
|
+
PACKAGE_NAMES = {"sdk": "oceanum-prax-sdk", "cli": "oceanum-prax-cli"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_snapshot(
|
|
24
|
+
snapshot_path: Path, expected_package: str
|
|
25
|
+
) -> tuple[bool, str | None]:
|
|
26
|
+
root = snapshot_path.resolve().parent
|
|
27
|
+
snapshot = json.loads(snapshot_path.read_text(encoding="utf-8"))
|
|
28
|
+
package = snapshot.get("package")
|
|
29
|
+
if package not in PACKAGE_NAMES or PACKAGE_NAMES[package] != expected_package:
|
|
30
|
+
raise ValueError("Snapshot package does not match the expected distribution")
|
|
31
|
+
if not re.fullmatch(r"[0-9a-f]{40}", snapshot.get("source_commit", "")):
|
|
32
|
+
raise ValueError("Snapshot source_commit must be a full Git SHA")
|
|
33
|
+
tooling_commit = snapshot.get("tooling_commit", snapshot["source_commit"])
|
|
34
|
+
if not isinstance(tooling_commit, str) or not re.fullmatch(
|
|
35
|
+
r"[0-9a-f]{40}", tooling_commit
|
|
36
|
+
):
|
|
37
|
+
raise ValueError("Snapshot tooling_commit must be a full Git SHA")
|
|
38
|
+
files = snapshot.get("files")
|
|
39
|
+
if not isinstance(files, dict) or not files:
|
|
40
|
+
raise ValueError("Snapshot must include its exported file hashes")
|
|
41
|
+
for relative, expected_hash in files.items():
|
|
42
|
+
path = PurePosixPath(relative)
|
|
43
|
+
if path.is_absolute() or ".." in path.parts:
|
|
44
|
+
raise ValueError(f"Unsafe exported path in snapshot: {relative}")
|
|
45
|
+
file_path = root.joinpath(*path.parts)
|
|
46
|
+
if file_path.is_symlink() or not file_path.is_file():
|
|
47
|
+
raise ValueError(f"Snapshot file is missing: {relative}")
|
|
48
|
+
actual_hash = hashlib.sha256(file_path.read_bytes()).hexdigest()
|
|
49
|
+
if actual_hash != expected_hash:
|
|
50
|
+
raise ValueError(f"Snapshot file hash mismatch: {relative}")
|
|
51
|
+
tooling_files = snapshot.get("tooling_files", {})
|
|
52
|
+
if not isinstance(tooling_files, dict) or any(
|
|
53
|
+
files.get(relative) != expected_hash
|
|
54
|
+
for relative, expected_hash in tooling_files.items()
|
|
55
|
+
):
|
|
56
|
+
raise ValueError("Snapshot tooling file hashes do not match exported files")
|
|
57
|
+
expected_files = set(files) | {"SOURCE_SNAPSHOT.json"}
|
|
58
|
+
actual_files = {
|
|
59
|
+
path.relative_to(root).as_posix()
|
|
60
|
+
for path in root.rglob("*")
|
|
61
|
+
if path.is_file()
|
|
62
|
+
and ".git" not in path.relative_to(root).parts
|
|
63
|
+
and ".venv" not in path.relative_to(root).parts
|
|
64
|
+
and "__pycache__" not in path.relative_to(root).parts
|
|
65
|
+
}
|
|
66
|
+
if actual_files != expected_files:
|
|
67
|
+
raise ValueError("Mirror contains files outside the validated snapshot")
|
|
68
|
+
project = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))[
|
|
69
|
+
"project"
|
|
70
|
+
]
|
|
71
|
+
if project.get("name") != expected_package:
|
|
72
|
+
raise ValueError("pyproject distribution name does not match snapshot")
|
|
73
|
+
|
|
74
|
+
release_tag = snapshot.get("release_tag")
|
|
75
|
+
release_version = snapshot.get("release_version")
|
|
76
|
+
if release_tag is None and release_version is None:
|
|
77
|
+
if project.get("version") != "0.0.0":
|
|
78
|
+
raise ValueError("Non-release snapshot must retain the development version")
|
|
79
|
+
return False, None
|
|
80
|
+
if (
|
|
81
|
+
not isinstance(release_tag, str)
|
|
82
|
+
or not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", release_tag)
|
|
83
|
+
or release_version != release_tag[1:]
|
|
84
|
+
or project.get("version") != release_version
|
|
85
|
+
):
|
|
86
|
+
raise ValueError("Snapshot release tag, version, and package metadata disagree")
|
|
87
|
+
return True, release_version
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def artifact_hashes(directory: Path) -> dict[str, str]:
|
|
91
|
+
artifacts = sorted(
|
|
92
|
+
path
|
|
93
|
+
for path in directory.iterdir()
|
|
94
|
+
if path.name.endswith(".whl") or path.name.endswith(".tar.gz")
|
|
95
|
+
)
|
|
96
|
+
if len(artifacts) != 2 or {path.suffix for path in artifacts} != {".whl", ".gz"}:
|
|
97
|
+
raise ValueError(f"Expected exactly one wheel and one sdist in {directory}")
|
|
98
|
+
return {
|
|
99
|
+
path.name: hashlib.sha256(path.read_bytes()).hexdigest() for path in artifacts
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def release_json(package: str, version: str) -> dict | None:
|
|
104
|
+
request = urllib.request.Request(
|
|
105
|
+
PYPI_JSON.format(package=package, version=version),
|
|
106
|
+
headers={"Accept": "application/json"},
|
|
107
|
+
)
|
|
108
|
+
try:
|
|
109
|
+
with urllib.request.urlopen(request, timeout=20) as response:
|
|
110
|
+
return json.load(response)
|
|
111
|
+
except urllib.error.HTTPError as error:
|
|
112
|
+
if error.code == 404:
|
|
113
|
+
return None
|
|
114
|
+
raise
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def files_to_publish(package: str, version: str, directory: Path) -> list[str]:
|
|
118
|
+
local = artifact_hashes(directory)
|
|
119
|
+
published = release_json(package, version)
|
|
120
|
+
if published is None:
|
|
121
|
+
return sorted(local)
|
|
122
|
+
remote = {
|
|
123
|
+
file["filename"]: file["digests"]["sha256"]
|
|
124
|
+
for file in published.get("urls", [])
|
|
125
|
+
}
|
|
126
|
+
if not set(remote).issubset(local) or any(
|
|
127
|
+
local[filename] != digest for filename, digest in remote.items()
|
|
128
|
+
):
|
|
129
|
+
raise ValueError(
|
|
130
|
+
f"PyPI already has conflicting files for {package} {version}; "
|
|
131
|
+
"refusing to overwrite"
|
|
132
|
+
)
|
|
133
|
+
return sorted(set(local) - set(remote))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def should_publish(package: str, version: str, directory: Path) -> bool:
|
|
137
|
+
return bool(files_to_publish(package, version, directory))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def wait_for_sdk(version: str, *, attempts: int = 30, delay: float = 10) -> None:
|
|
141
|
+
for attempt in range(attempts):
|
|
142
|
+
release = release_json(PACKAGE_NAMES["sdk"], version)
|
|
143
|
+
urls = release.get("urls", []) if release else []
|
|
144
|
+
has_wheel = any(item.get("packagetype") == "bdist_wheel" for item in urls)
|
|
145
|
+
has_sdist = any(item.get("packagetype") == "sdist" for item in urls)
|
|
146
|
+
if has_wheel and has_sdist:
|
|
147
|
+
return
|
|
148
|
+
if attempt + 1 < attempts:
|
|
149
|
+
time.sleep(delay)
|
|
150
|
+
raise ValueError(f"Matching SDK {version} is not fully available on PyPI")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def write_output(name: str, value: str) -> None:
|
|
154
|
+
output = os.environ.get("GITHUB_OUTPUT")
|
|
155
|
+
if output:
|
|
156
|
+
with open(output, "a", encoding="utf-8") as stream:
|
|
157
|
+
stream.write(f"{name}={value}\n")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main() -> int:
|
|
161
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
162
|
+
parser.add_argument("--snapshot", type=Path, required=True)
|
|
163
|
+
parser.add_argument("--expected-package", required=True)
|
|
164
|
+
parser.add_argument("--validate-only", action="store_true")
|
|
165
|
+
parser.add_argument("--distribution-dir", type=Path)
|
|
166
|
+
parser.add_argument("--publish-dir", type=Path)
|
|
167
|
+
parser.add_argument("--require-sdk", action="store_true")
|
|
168
|
+
args = parser.parse_args()
|
|
169
|
+
try:
|
|
170
|
+
is_release, version = validate_snapshot(args.snapshot, args.expected_package)
|
|
171
|
+
write_output("is_release", str(is_release).lower())
|
|
172
|
+
if args.validate_only or not is_release:
|
|
173
|
+
print(f"is_release={str(is_release).lower()}")
|
|
174
|
+
return 0
|
|
175
|
+
if args.distribution_dir is None:
|
|
176
|
+
raise ValueError("--distribution-dir is required for publishing preflight")
|
|
177
|
+
if args.require_sdk:
|
|
178
|
+
assert version is not None
|
|
179
|
+
wait_for_sdk(version)
|
|
180
|
+
assert version is not None
|
|
181
|
+
files = files_to_publish(args.expected_package, version, args.distribution_dir)
|
|
182
|
+
publish = bool(files)
|
|
183
|
+
if args.publish_dir:
|
|
184
|
+
args.publish_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
for filename in files:
|
|
186
|
+
shutil.copy2(
|
|
187
|
+
args.distribution_dir / filename, args.publish_dir / filename
|
|
188
|
+
)
|
|
189
|
+
write_output("should_publish", str(publish).lower())
|
|
190
|
+
print(f"should_publish={str(publish).lower()}")
|
|
191
|
+
return 0
|
|
192
|
+
except (OSError, ValueError, json.JSONDecodeError, KeyError) as error:
|
|
193
|
+
print(f"PyPI release preflight failed: {error}", file=sys.stderr)
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == "__main__":
|
|
198
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
name: Publish SDK to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "SOURCE_SNAPSHOT.json"
|
|
8
|
+
- "LICENSE"
|
|
9
|
+
- "pyproject.toml"
|
|
10
|
+
- "src/**"
|
|
11
|
+
- ".github/workflows/publish.yml"
|
|
12
|
+
- ".github/scripts/check_pypi_release.py"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: pypi-${{ github.repository }}
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
publish:
|
|
24
|
+
if: github.ref == 'refs/heads/main'
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment:
|
|
27
|
+
name: pypi
|
|
28
|
+
url: https://pypi.org/project/oceanum-prax-sdk/
|
|
29
|
+
permissions:
|
|
30
|
+
contents: read
|
|
31
|
+
id-token: write
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- name: Set up uv
|
|
36
|
+
uses: astral-sh/setup-uv@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
enable-cache: false
|
|
40
|
+
|
|
41
|
+
- id: release
|
|
42
|
+
name: Validate release snapshot provenance
|
|
43
|
+
run: python .github/scripts/check_pypi_release.py --snapshot SOURCE_SNAPSHOT.json --expected-package oceanum-prax-sdk --validate-only
|
|
44
|
+
|
|
45
|
+
- name: Build and validate distributions
|
|
46
|
+
if: steps.release.outputs.is_release == 'true'
|
|
47
|
+
run: |
|
|
48
|
+
mkdir -p "$RUNNER_TEMP/package-source"
|
|
49
|
+
git archive HEAD | tar -x -C "$RUNNER_TEMP/package-source"
|
|
50
|
+
uv build --no-sources "$RUNNER_TEMP/package-source" --out-dir "$RUNNER_TEMP/dist"
|
|
51
|
+
uvx --from twine twine check "$RUNNER_TEMP"/dist/*
|
|
52
|
+
|
|
53
|
+
- id: preflight
|
|
54
|
+
name: Prepare only missing immutable PyPI files
|
|
55
|
+
if: steps.release.outputs.is_release == 'true'
|
|
56
|
+
run: python .github/scripts/check_pypi_release.py --snapshot SOURCE_SNAPSHOT.json --expected-package oceanum-prax-sdk --distribution-dir "$RUNNER_TEMP/dist" --publish-dir "$RUNNER_TEMP/dist-to-upload"
|
|
57
|
+
|
|
58
|
+
- name: Publish to PyPI
|
|
59
|
+
if: steps.preflight.outputs.should_publish == 'true'
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
with:
|
|
62
|
+
packages-dir: ${{ runner.temp }}/dist-to-upload
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022, Oceanum Developers
|
|
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.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: oceanum-prax-sdk
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Typed clients and data models for PRAX platform
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: api,client,prax,pydantic
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: httpx>=0.27.0
|
|
13
|
+
Requires-Dist: pydantic-settings>=2.0
|
|
14
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: datamodel-code-generator>=0.25.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: mypy>=1.11.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pre-commit>=3.5.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
24
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest-timeout>=2.3.1; extra == 'test'
|
|
26
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# prax (PRAX SDK)
|
|
30
|
+
|
|
31
|
+
Typed HTTP clients, API models, and authored input helpers for PRAX applications.
|
|
32
|
+
The Python distribution and import name are **`prax`**.
|
|
33
|
+
|
|
34
|
+
## Working with PRAX
|
|
35
|
+
|
|
36
|
+
Use the SDK to:
|
|
37
|
+
|
|
38
|
+
- Read exact service-instance health, metrics and usage, and stream logs with
|
|
39
|
+
`PraxAPIClient`.
|
|
40
|
+
- Read and compare authored ProjectSpec revisions with bounded history operations.
|
|
41
|
+
- Validate and manipulate pipeline/stage inputs with `prax.spec` and
|
|
42
|
+
`prax.project_spec_workflow`.
|
|
43
|
+
- Work with typed request/response data through `prax.models`.
|
|
44
|
+
|
|
45
|
+
The asynchronous client supports context-manager cleanup, request timeouts, and
|
|
46
|
+
bounded pagination. Authorize requests with an API token for your environment;
|
|
47
|
+
resource access follows your account's permissions.
|
|
48
|
+
|
|
49
|
+
## Example
|
|
50
|
+
|
|
51
|
+
Set `PRAX_API_URL` to your PRAX endpoint and `PRAX_API_TOKEN` to your API token.
|
|
52
|
+
Substitute a project name and revision number accessible to your account.
|
|
53
|
+
|
|
54
|
+
<!-- sdk-http-example:start -->
|
|
55
|
+
```python
|
|
56
|
+
import asyncio
|
|
57
|
+
import os
|
|
58
|
+
|
|
59
|
+
from prax import PraxAPIClient
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def main():
|
|
63
|
+
async with PraxAPIClient(
|
|
64
|
+
os.environ["PRAX_API_URL"], token=os.environ["PRAX_API_TOKEN"]
|
|
65
|
+
) as client:
|
|
66
|
+
revision = await client.get_project_spec_revision("forecast-project", 1)
|
|
67
|
+
print(revision.model_dump_json())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
asyncio.run(main())
|
|
72
|
+
```
|
|
73
|
+
<!-- sdk-http-example:end -->
|
|
74
|
+
|
|
75
|
+
## Following service-instance logs
|
|
76
|
+
|
|
77
|
+
Use the complete project, stage, service and stage-incarnation identity returned
|
|
78
|
+
by the API. Open logs with `async with`, not `await`: the context owns the response
|
|
79
|
+
and closes it on completion, an early exit, error or cancellation.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
async with client.stream_service_instance_logs(
|
|
83
|
+
project_ref="forecast-project",
|
|
84
|
+
stage_ref="production",
|
|
85
|
+
service_name="api",
|
|
86
|
+
stage_incarnation=stage_incarnation,
|
|
87
|
+
org="oceanum",
|
|
88
|
+
follow=True,
|
|
89
|
+
tail=100,
|
|
90
|
+
) as response:
|
|
91
|
+
async for line in response.aiter_lines():
|
|
92
|
+
print(line)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Read the response only inside this context. Follow has no idle read deadline;
|
|
96
|
+
connection, write and pool timeouts remain those of the client. Snapshot requests
|
|
97
|
+
retain the normal read timeout. Injected/shared HTTP clients remain caller-owned.
|
|
98
|
+
No status lookup or repeated reconnection is needed to receive a live line.
|
|
99
|
+
|
|
100
|
+
## Errors
|
|
101
|
+
|
|
102
|
+
Client operations raise `PraxAPIError`. Use its `.status`, `.code`, and `.reason`
|
|
103
|
+
attributes to handle errors programmatically.
|
|
104
|
+
|
|
105
|
+
Catch this exception around awaited client operations to distinguish API responses
|
|
106
|
+
from transport failures and decide whether to retry.
|
|
107
|
+
|
|
108
|
+
## Requirements
|
|
109
|
+
|
|
110
|
+
Python **3.11+** is required. Runtime dependencies are Pydantic, Pydantic Settings,
|
|
111
|
+
HTTPX, and PyYAML; version bounds are declared in `pyproject.toml`.
|
|
112
|
+
|
|
113
|
+
## Versions
|
|
114
|
+
|
|
115
|
+
The SDK and CLI share the same release version. Each CLI release depends on the
|
|
116
|
+
matching `prax` SDK release. To inspect your installed SDK version:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from importlib.metadata import version
|
|
120
|
+
|
|
121
|
+
print(version("oceanum-prax-sdk"))
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## See also
|
|
125
|
+
|
|
126
|
+
- [PRAX command-line interface](https://github.com/oceanum/prax-cli)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# prax (PRAX SDK)
|
|
2
|
+
|
|
3
|
+
Typed HTTP clients, API models, and authored input helpers for PRAX applications.
|
|
4
|
+
The Python distribution and import name are **`prax`**.
|
|
5
|
+
|
|
6
|
+
## Working with PRAX
|
|
7
|
+
|
|
8
|
+
Use the SDK to:
|
|
9
|
+
|
|
10
|
+
- Read exact service-instance health, metrics and usage, and stream logs with
|
|
11
|
+
`PraxAPIClient`.
|
|
12
|
+
- Read and compare authored ProjectSpec revisions with bounded history operations.
|
|
13
|
+
- Validate and manipulate pipeline/stage inputs with `prax.spec` and
|
|
14
|
+
`prax.project_spec_workflow`.
|
|
15
|
+
- Work with typed request/response data through `prax.models`.
|
|
16
|
+
|
|
17
|
+
The asynchronous client supports context-manager cleanup, request timeouts, and
|
|
18
|
+
bounded pagination. Authorize requests with an API token for your environment;
|
|
19
|
+
resource access follows your account's permissions.
|
|
20
|
+
|
|
21
|
+
## Example
|
|
22
|
+
|
|
23
|
+
Set `PRAX_API_URL` to your PRAX endpoint and `PRAX_API_TOKEN` to your API token.
|
|
24
|
+
Substitute a project name and revision number accessible to your account.
|
|
25
|
+
|
|
26
|
+
<!-- sdk-http-example:start -->
|
|
27
|
+
```python
|
|
28
|
+
import asyncio
|
|
29
|
+
import os
|
|
30
|
+
|
|
31
|
+
from prax import PraxAPIClient
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def main():
|
|
35
|
+
async with PraxAPIClient(
|
|
36
|
+
os.environ["PRAX_API_URL"], token=os.environ["PRAX_API_TOKEN"]
|
|
37
|
+
) as client:
|
|
38
|
+
revision = await client.get_project_spec_revision("forecast-project", 1)
|
|
39
|
+
print(revision.model_dump_json())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
<!-- sdk-http-example:end -->
|
|
46
|
+
|
|
47
|
+
## Following service-instance logs
|
|
48
|
+
|
|
49
|
+
Use the complete project, stage, service and stage-incarnation identity returned
|
|
50
|
+
by the API. Open logs with `async with`, not `await`: the context owns the response
|
|
51
|
+
and closes it on completion, an early exit, error or cancellation.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
async with client.stream_service_instance_logs(
|
|
55
|
+
project_ref="forecast-project",
|
|
56
|
+
stage_ref="production",
|
|
57
|
+
service_name="api",
|
|
58
|
+
stage_incarnation=stage_incarnation,
|
|
59
|
+
org="oceanum",
|
|
60
|
+
follow=True,
|
|
61
|
+
tail=100,
|
|
62
|
+
) as response:
|
|
63
|
+
async for line in response.aiter_lines():
|
|
64
|
+
print(line)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Read the response only inside this context. Follow has no idle read deadline;
|
|
68
|
+
connection, write and pool timeouts remain those of the client. Snapshot requests
|
|
69
|
+
retain the normal read timeout. Injected/shared HTTP clients remain caller-owned.
|
|
70
|
+
No status lookup or repeated reconnection is needed to receive a live line.
|
|
71
|
+
|
|
72
|
+
## Errors
|
|
73
|
+
|
|
74
|
+
Client operations raise `PraxAPIError`. Use its `.status`, `.code`, and `.reason`
|
|
75
|
+
attributes to handle errors programmatically.
|
|
76
|
+
|
|
77
|
+
Catch this exception around awaited client operations to distinguish API responses
|
|
78
|
+
from transport failures and decide whether to retry.
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
Python **3.11+** is required. Runtime dependencies are Pydantic, Pydantic Settings,
|
|
83
|
+
HTTPX, and PyYAML; version bounds are declared in `pyproject.toml`.
|
|
84
|
+
|
|
85
|
+
## Versions
|
|
86
|
+
|
|
87
|
+
The SDK and CLI share the same release version. Each CLI release depends on the
|
|
88
|
+
matching `prax` SDK release. To inspect your installed SDK version:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from importlib.metadata import version
|
|
92
|
+
|
|
93
|
+
print(version("oceanum-prax-sdk"))
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## See also
|
|
97
|
+
|
|
98
|
+
- [PRAX command-line interface](https://github.com/oceanum/prax-cli)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": {
|
|
3
|
+
".github/scripts/check_pypi_release.py": "87c9e2d2834541e9fbf631d2b281514952fcd96abf9a2937eb6d007a93c063aa",
|
|
4
|
+
".github/workflows/publish.yml": "236641898fe29afb92a512999b1881a986d060f10f4d85ebf59d344e51f4d3bb",
|
|
5
|
+
"LICENSE": "cc79a4f8cc49e9db712786cf7df8a962f1da42ea390be5fecededd9eafaaf3bb",
|
|
6
|
+
"README.md": "25e9f0767400767cb6b0bccc44011cae03d3abd78c3769ec4a887faced5eed55",
|
|
7
|
+
"pyproject.toml": "d8ef2786a4d8d37372fd1ce940437b79ba04507cc0c87f83aec35ae2a2c81d52",
|
|
8
|
+
"src/prax/__init__.py": "cd41a84b73cc1cf0e65dc7904fde540e443d22502d3a34675170bfeded4e081e",
|
|
9
|
+
"src/prax/_version.py": "8f5323cc90d87b05740ede2dea97a2e4f8d09c9a956aae3d945e182b8c79e327",
|
|
10
|
+
"src/prax/api.py": "c2d51329286e5aa66dbe06e2f06dcf6665da4f48220b0c211c85b10ebc96603a",
|
|
11
|
+
"src/prax/files/__init__.py": "3333a4e8da259746c5ee671d066156296165d40c0c601f32a7ae8f3004271d01",
|
|
12
|
+
"src/prax/files/base.py": "4fcce9d2c44babfacbdd338a30210b8181fe7e6f8d0aa8bd561f90f13bb47469",
|
|
13
|
+
"src/prax/models/__init__.py": "2836f33525efeeca58839e3de431c4eb4ee3d2f76f6e284c7f5b3c0548007283",
|
|
14
|
+
"src/prax/project_spec_workflow.py": "9b3d4218f2e7c34c5573e4f27497ff6a4b41e5ea168fd823e8a9d9780be11f22",
|
|
15
|
+
"src/prax/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
16
|
+
"src/prax/settings.py": "8051b865132b9bd63ed2fed1956cc21a06ace4e49526c5d06acd32ce7f31cdbf",
|
|
17
|
+
"src/prax/spec/__init__.py": "8e4809da07c289b64c19fa712ac278d5b3a912ea2729ba4b324e1ec2b942071e",
|
|
18
|
+
"src/prax/spec/_base_types.py": "98f4151503e40c3f8e1cdf9a546c728ebf527db0f1438cb5874972f259d0a617",
|
|
19
|
+
"src/prax/spec/_workflow.py": "42a4861ac84a293417f4111ab0d188e40f50a959dc1bae33662be2b02dc52f58",
|
|
20
|
+
"src/prax/spec/pipeline.py": "4dc8d660d8e695906897973c9bb5ab3a9e1a81899db104f74eceb2a4eec7ea15",
|
|
21
|
+
"src/prax/spec/resources.py": "e659a7c2f9d443eb330a9537dd2ec7455fa479f863c1b69af2c590548db44c11",
|
|
22
|
+
"src/prax/spec/secret_refs.py": "00e0b8f9f9a4da4d8d4405bc9b94eddaf287883d4b103381145a927b9ff0c3ee",
|
|
23
|
+
"src/prax/spec/source_refs.py": "1fe0dcd6c60cfc618bdb86fa5241ea67849813d808462dac4ede1bed6ed1e185",
|
|
24
|
+
"src/prax/spec/stage.py": "8f77d8fe58a0f2b98336efd9826920e9ad59ded76bd9baf88a5d59eead3a2532",
|
|
25
|
+
"src/prax/utils.py": "81185a7529283b19999da234e8246f761fceaf027e46f9a139f7d638b5e14494"
|
|
26
|
+
},
|
|
27
|
+
"package": "sdk",
|
|
28
|
+
"release_tag": "v0.3.2",
|
|
29
|
+
"release_version": "0.3.2",
|
|
30
|
+
"source_commit": "b40191a0a15e5459c7e5b672a41cb80326e33c1b",
|
|
31
|
+
"source_files": {
|
|
32
|
+
".github/scripts/check_pypi_release.py": "49454c3b95615cba02c209d6ed63e2f27bb5507553f987b0bffcd095a7ad7f76",
|
|
33
|
+
".github/workflows/publish.yml": "236641898fe29afb92a512999b1881a986d060f10f4d85ebf59d344e51f4d3bb",
|
|
34
|
+
"LICENSE": "cc79a4f8cc49e9db712786cf7df8a962f1da42ea390be5fecededd9eafaaf3bb",
|
|
35
|
+
"README.md": "25e9f0767400767cb6b0bccc44011cae03d3abd78c3769ec4a887faced5eed55",
|
|
36
|
+
"pyproject.toml": "cce473f073295243a84873b14a09a8bf477d5d3b5a462b27d6f80ff64c186740",
|
|
37
|
+
"src/prax/__init__.py": "cd41a84b73cc1cf0e65dc7904fde540e443d22502d3a34675170bfeded4e081e",
|
|
38
|
+
"src/prax/_version.py": "8f5323cc90d87b05740ede2dea97a2e4f8d09c9a956aae3d945e182b8c79e327",
|
|
39
|
+
"src/prax/api.py": "c2d51329286e5aa66dbe06e2f06dcf6665da4f48220b0c211c85b10ebc96603a",
|
|
40
|
+
"src/prax/files/__init__.py": "3333a4e8da259746c5ee671d066156296165d40c0c601f32a7ae8f3004271d01",
|
|
41
|
+
"src/prax/files/base.py": "4fcce9d2c44babfacbdd338a30210b8181fe7e6f8d0aa8bd561f90f13bb47469",
|
|
42
|
+
"src/prax/models/__init__.py": "2836f33525efeeca58839e3de431c4eb4ee3d2f76f6e284c7f5b3c0548007283",
|
|
43
|
+
"src/prax/project_spec_workflow.py": "9b3d4218f2e7c34c5573e4f27497ff6a4b41e5ea168fd823e8a9d9780be11f22",
|
|
44
|
+
"src/prax/py.typed": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
45
|
+
"src/prax/settings.py": "8051b865132b9bd63ed2fed1956cc21a06ace4e49526c5d06acd32ce7f31cdbf",
|
|
46
|
+
"src/prax/spec/__init__.py": "8e4809da07c289b64c19fa712ac278d5b3a912ea2729ba4b324e1ec2b942071e",
|
|
47
|
+
"src/prax/spec/_base_types.py": "98f4151503e40c3f8e1cdf9a546c728ebf527db0f1438cb5874972f259d0a617",
|
|
48
|
+
"src/prax/spec/_workflow.py": "42a4861ac84a293417f4111ab0d188e40f50a959dc1bae33662be2b02dc52f58",
|
|
49
|
+
"src/prax/spec/pipeline.py": "4dc8d660d8e695906897973c9bb5ab3a9e1a81899db104f74eceb2a4eec7ea15",
|
|
50
|
+
"src/prax/spec/resources.py": "e659a7c2f9d443eb330a9537dd2ec7455fa479f863c1b69af2c590548db44c11",
|
|
51
|
+
"src/prax/spec/secret_refs.py": "00e0b8f9f9a4da4d8d4405bc9b94eddaf287883d4b103381145a927b9ff0c3ee",
|
|
52
|
+
"src/prax/spec/source_refs.py": "1fe0dcd6c60cfc618bdb86fa5241ea67849813d808462dac4ede1bed6ed1e185",
|
|
53
|
+
"src/prax/spec/stage.py": "8f77d8fe58a0f2b98336efd9826920e9ad59ded76bd9baf88a5d59eead3a2532",
|
|
54
|
+
"src/prax/utils.py": "81185a7529283b19999da234e8246f761fceaf027e46f9a139f7d638b5e14494"
|
|
55
|
+
},
|
|
56
|
+
"tooling_commit": "4277b49c22b7d99b6ce4b8ec7afb253a7e843c50",
|
|
57
|
+
"tooling_files": {
|
|
58
|
+
".github/scripts/check_pypi_release.py": "87c9e2d2834541e9fbf631d2b281514952fcd96abf9a2937eb6d007a93c063aa"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oceanum-prax-sdk"
|
|
7
|
+
# Development placeholder; release exports are stamped from the monorepo tag.
|
|
8
|
+
version = "0.3.2"
|
|
9
|
+
description = "Typed clients and data models for PRAX platform"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
keywords = ["prax", "pydantic", "client", "api"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
dependencies = [
|
|
22
|
+
"pydantic>=2.0,<3.0",
|
|
23
|
+
"pydantic-settings>=2.0",
|
|
24
|
+
"httpx>=0.27.0",
|
|
25
|
+
"PyYAML>=6.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = [
|
|
30
|
+
"pytest>=8.0.0",
|
|
31
|
+
"pytest-cov>=4.1.0",
|
|
32
|
+
"pytest-asyncio>=0.23.0",
|
|
33
|
+
"pytest-timeout>=2.3.1",
|
|
34
|
+
]
|
|
35
|
+
dev = [
|
|
36
|
+
"ruff>=0.6.0",
|
|
37
|
+
"mypy>=1.11.0",
|
|
38
|
+
"pre-commit>=3.5.0",
|
|
39
|
+
"types-PyYAML>=6.0.12",
|
|
40
|
+
"datamodel-code-generator>=0.25.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/prax"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
minversion = "8.0"
|
|
48
|
+
addopts = [
|
|
49
|
+
"-ra",
|
|
50
|
+
"-q",
|
|
51
|
+
"--strict-markers",
|
|
52
|
+
"--strict-config",
|
|
53
|
+
"--tb=short",
|
|
54
|
+
]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
57
|
+
python_classes = ["Test*"]
|
|
58
|
+
python_functions = ["test_*"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
target-version = "py311"
|
|
62
|
+
line-length = 100
|
|
63
|
+
exclude = [
|
|
64
|
+
".git",
|
|
65
|
+
"build",
|
|
66
|
+
"dist",
|
|
67
|
+
".venv",
|
|
68
|
+
"__pycache__",
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = [
|
|
73
|
+
"E", # pycodestyle errors
|
|
74
|
+
"W", # pycodestyle warnings
|
|
75
|
+
"F", # pyflakes
|
|
76
|
+
"I", # isort
|
|
77
|
+
"N", # pep8-naming
|
|
78
|
+
"UP", # pyupgrade
|
|
79
|
+
"B", # flake8-bugbear
|
|
80
|
+
"C4", # flake8-comprehensions
|
|
81
|
+
"SIM", # flake8-simplify
|
|
82
|
+
"RET", # flake8-return
|
|
83
|
+
"ARG", # flake8-unused-arguments
|
|
84
|
+
"PTH", # flake8-use-pathlib
|
|
85
|
+
"PL", # pylint
|
|
86
|
+
"RUF", # Ruff-specific
|
|
87
|
+
]
|
|
88
|
+
ignore = [
|
|
89
|
+
"E501", # Line too long (handled by formatter)
|
|
90
|
+
"B008", # Function call in defaults (Pydantic pattern)
|
|
91
|
+
"PLR0913", # Too many arguments
|
|
92
|
+
"ARG001", # Unused function argument
|
|
93
|
+
"ARG002", # Unused method argument
|
|
94
|
+
"PLW1641", # Object does not implement __hash__ (Pydantic models)
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.per-file-ignores]
|
|
98
|
+
"tests/**/*.py" = ["F401", "F403"]
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.isort]
|
|
101
|
+
known-first-party = ["prax"]
|
|
102
|
+
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
|
|
103
|
+
|
|
104
|
+
[tool.ruff.format]
|
|
105
|
+
quote-style = "double"
|
|
106
|
+
indent-style = "space"
|
|
107
|
+
line-ending = "auto"
|
|
108
|
+
|
|
109
|
+
[tool.mypy]
|
|
110
|
+
python_version = "3.12"
|
|
111
|
+
exclude = ["src/prax/models/"]
|
|
112
|
+
warn_return_any = true
|
|
113
|
+
warn_unused_configs = true
|
|
114
|
+
disallow_untyped_defs = false
|
|
115
|
+
check_untyped_defs = true
|
|
116
|
+
warn_redundant_casts = true
|
|
117
|
+
warn_unused_ignores = true
|
|
118
|
+
warn_no_return = true
|
|
119
|
+
strict_optional = true
|
|
120
|
+
pretty = true
|