gentic-sdk 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gentic_sdk-0.1.0/.gitignore +4 -0
- gentic_sdk-0.1.0/PKG-INFO +24 -0
- gentic_sdk-0.1.0/README.md +15 -0
- gentic_sdk-0.1.0/pyproject.toml +26 -0
- gentic_sdk-0.1.0/scripts/generate-models.sh +16 -0
- gentic_sdk-0.1.0/scripts/publish.sh +59 -0
- gentic_sdk-0.1.0/src/gentic_sdk/__init__.py +4 -0
- gentic_sdk-0.1.0/src/gentic_sdk/client.py +116 -0
- gentic_sdk-0.1.0/src/gentic_sdk/exceptions.py +5 -0
- gentic_sdk-0.1.0/src/gentic_sdk/models.py +1490 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/__init__.py +18 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/account.py +37 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/activity.py +15 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/auth.py +34 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/categories.py +15 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/cli.py +13 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/health.py +10 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/library.py +40 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/notifications.py +22 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/oauth.py +16 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/profiles.py +34 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/projects.py +13 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/recommend.py +22 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/skills.py +100 -0
- gentic_sdk-0.1.0/src/gentic_sdk/resources/subscriptions.py +27 -0
- gentic_sdk-0.1.0/tests/test_client.py +377 -0
- gentic_sdk-0.1.0/uv.lock +768 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: gentic-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the GenticHub API
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: httpx>=0.27
|
|
7
|
+
Requires-Dist: pydantic[email]>=2.7
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# gentic-sdk
|
|
11
|
+
|
|
12
|
+
Python client for the GenticHub API.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from gentic_sdk import GenticHub
|
|
16
|
+
|
|
17
|
+
hub = GenticHub("http://localhost:8000/api")
|
|
18
|
+
hub.sign_in("ana@example.com", "a-password")
|
|
19
|
+
|
|
20
|
+
skill = hub.skills.get_public("pdf-processing")
|
|
21
|
+
print(skill.skill.name)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Every call returns a model from `gentic_sdk.models`. A call that the API refuses raises `GenticHubError` with the `status` and the `detail` the API sent (for example `invalid_credentials`). Over the API's rate limit the status is 429 and the `detail` is `too_many_requests` (see `docs/rate-limiting.md`); wait before trying again.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# gentic-sdk
|
|
2
|
+
|
|
3
|
+
Python client for the GenticHub API.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from gentic_sdk import GenticHub
|
|
7
|
+
|
|
8
|
+
hub = GenticHub("http://localhost:8000/api")
|
|
9
|
+
hub.sign_in("ana@example.com", "a-password")
|
|
10
|
+
|
|
11
|
+
skill = hub.skills.get_public("pdf-processing")
|
|
12
|
+
print(skill.skill.name)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Every call returns a model from `gentic_sdk.models`. A call that the API refuses raises `GenticHubError` with the `status` and the `detail` the API sent (for example `invalid_credentials`). Over the API's rate limit the status is 429 and the `detail` is `too_many_requests` (see `docs/rate-limiting.md`); wait before trying again.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "gentic-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python SDK for the GenticHub API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"httpx>=0.27",
|
|
9
|
+
"pydantic[email]>=2.7",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = [
|
|
14
|
+
"datamodel-code-generator>=0.25",
|
|
15
|
+
"pytest>=8",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["hatchling"]
|
|
20
|
+
build-backend = "hatchling.build"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/gentic_sdk"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regenerates src/gentic_sdk/models.py from the API description. Run it after api/openapi.yaml changes.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
cd "$(dirname "$0")/.."
|
|
6
|
+
|
|
7
|
+
uv run datamodel-codegen \
|
|
8
|
+
--input ../../api/openapi.yaml \
|
|
9
|
+
--input-file-type openapi \
|
|
10
|
+
--output src/gentic_sdk/models.py \
|
|
11
|
+
--output-model-type pydantic_v2.BaseModel \
|
|
12
|
+
--target-python-version 3.11 \
|
|
13
|
+
--use-annotated \
|
|
14
|
+
--use-standard-collections \
|
|
15
|
+
--use-union-operator \
|
|
16
|
+
--disable-timestamp
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Publishes the SDK. Usage: scripts/publish.sh [--public] [--dry-run]
|
|
3
|
+
# By default it uploads to TestPyPI (https://test.pypi.org), for trying a release; with --public it uploads to the real PyPI.
|
|
4
|
+
# SDK_PUBLISH_URL, when set, replaces both with the upload URL of another index (a private one, for example).
|
|
5
|
+
# SDK_CHECK_URL (optional) is the index URL to look in, to skip files that are already published; PyPI and TestPyPI set it themselves.
|
|
6
|
+
# Credentials: UV_PUBLISH_TOKEN, or UV_PUBLISH_USERNAME and UV_PUBLISH_PASSWORD. When they are not set the script asks for a token.
|
|
7
|
+
# TestPyPI and PyPI are separate sites: each needs its own account and token.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
cd "$(dirname "$0")/.."
|
|
11
|
+
|
|
12
|
+
dry_run_flag=()
|
|
13
|
+
public=false
|
|
14
|
+
for argument in "$@"; do
|
|
15
|
+
if [[ "$argument" == "--dry-run" ]]; then
|
|
16
|
+
dry_run_flag=(--dry-run)
|
|
17
|
+
elif [[ "$argument" == "--public" ]]; then
|
|
18
|
+
public=true
|
|
19
|
+
fi
|
|
20
|
+
done
|
|
21
|
+
|
|
22
|
+
if [[ "$public" == true ]]; then
|
|
23
|
+
default_publish_url="https://upload.pypi.org/legacy/"
|
|
24
|
+
default_check_url="https://pypi.org/simple/"
|
|
25
|
+
else
|
|
26
|
+
default_publish_url="https://test.pypi.org/legacy/"
|
|
27
|
+
default_check_url="https://test.pypi.org/simple/"
|
|
28
|
+
fi
|
|
29
|
+
if [[ -z "${SDK_PUBLISH_URL:-}" ]]; then
|
|
30
|
+
SDK_PUBLISH_URL="$default_publish_url"
|
|
31
|
+
SDK_CHECK_URL="${SDK_CHECK_URL:-$default_check_url}"
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
check_url_flag=()
|
|
35
|
+
if [[ -n "${SDK_CHECK_URL:-}" ]]; then
|
|
36
|
+
check_url_flag=(--check-url "$SDK_CHECK_URL")
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
if [[ -z "${UV_PUBLISH_TOKEN:-}" && -z "${UV_PUBLISH_PASSWORD:-}" ]]; then
|
|
40
|
+
if [[ ! -t 0 ]]; then
|
|
41
|
+
echo "No credentials for the index, and there is no terminal to ask for them. Set UV_PUBLISH_TOKEN." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
read -r -s -p "Token for ${SDK_PUBLISH_URL}: " UV_PUBLISH_TOKEN
|
|
45
|
+
echo
|
|
46
|
+
if [[ -z "$UV_PUBLISH_TOKEN" ]]; then
|
|
47
|
+
echo "No token entered." >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
export UV_PUBLISH_TOKEN
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
echo "Publishing to $SDK_PUBLISH_URL"
|
|
54
|
+
|
|
55
|
+
uv run pytest -q
|
|
56
|
+
rm -rf dist
|
|
57
|
+
uv build --out-dir dist
|
|
58
|
+
|
|
59
|
+
uv publish --publish-url "$SDK_PUBLISH_URL" ${check_url_flag[@]+"${check_url_flag[@]}"} ${dry_run_flag[@]+"${dry_run_flag[@]}"} dist/*
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Any, TypeVar
|
|
5
|
+
from urllib.parse import quote
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from pydantic import BaseModel, TypeAdapter
|
|
9
|
+
|
|
10
|
+
from gentic_sdk import models
|
|
11
|
+
from gentic_sdk.exceptions import GenticHubError
|
|
12
|
+
from gentic_sdk.resources.account import AccountApi
|
|
13
|
+
from gentic_sdk.resources.activity import ActivityApi
|
|
14
|
+
from gentic_sdk.resources.auth import AuthApi
|
|
15
|
+
from gentic_sdk.resources.categories import CategoriesApi
|
|
16
|
+
from gentic_sdk.resources.cli import CliApi
|
|
17
|
+
from gentic_sdk.resources.health import HealthApi
|
|
18
|
+
from gentic_sdk.resources.library import LibraryApi
|
|
19
|
+
from gentic_sdk.resources.oauth import OauthApi
|
|
20
|
+
from gentic_sdk.resources.notifications import NotificationsApi
|
|
21
|
+
from gentic_sdk.resources.profiles import ProfilesApi
|
|
22
|
+
from gentic_sdk.resources.projects import ProjectsApi
|
|
23
|
+
from gentic_sdk.resources.recommend import RecommendApi
|
|
24
|
+
from gentic_sdk.resources.skills import SkillsApi
|
|
25
|
+
from gentic_sdk.resources.subscriptions import SubscriptionsApi
|
|
26
|
+
|
|
27
|
+
Result = TypeVar("Result")
|
|
28
|
+
TokenSource = str | Callable[[], str | None]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@lru_cache
|
|
32
|
+
def adapter_for(result_type: Any) -> TypeAdapter[Any]:
|
|
33
|
+
return TypeAdapter(result_type)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class GenticHub:
|
|
37
|
+
def __init__(self, base_url: str, token: TokenSource | None = None, timeout: float = 30.0, http_client: httpx.Client | None = None) -> None:
|
|
38
|
+
self.token = token
|
|
39
|
+
self.http_client = http_client or httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout)
|
|
40
|
+
self.health = HealthApi(self)
|
|
41
|
+
self.auth = AuthApi(self)
|
|
42
|
+
self.skills = SkillsApi(self)
|
|
43
|
+
self.categories = CategoriesApi(self)
|
|
44
|
+
self.cli = CliApi(self)
|
|
45
|
+
self.library = LibraryApi(self)
|
|
46
|
+
self.account = AccountApi(self)
|
|
47
|
+
self.activity = ActivityApi(self)
|
|
48
|
+
self.notifications = NotificationsApi(self)
|
|
49
|
+
self.profiles = ProfilesApi(self)
|
|
50
|
+
self.projects = ProjectsApi(self)
|
|
51
|
+
self.oauth = OauthApi(self)
|
|
52
|
+
self.subscriptions = SubscriptionsApi(self)
|
|
53
|
+
self.recommend = RecommendApi(self)
|
|
54
|
+
|
|
55
|
+
def __enter__(self) -> "GenticHub":
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(self, *_exception: object) -> None:
|
|
59
|
+
self.close()
|
|
60
|
+
|
|
61
|
+
def close(self) -> None:
|
|
62
|
+
self.http_client.close()
|
|
63
|
+
|
|
64
|
+
def sign_in(self, email: str, password: str) -> models.TokenResponse:
|
|
65
|
+
token = self.auth.token(models.TokenRequest(email=email, password=password))
|
|
66
|
+
if not isinstance(token, models.TokenResponse):
|
|
67
|
+
raise GenticHubError(500, "unexpected_answer")
|
|
68
|
+
self.token = token.access_token
|
|
69
|
+
return token
|
|
70
|
+
|
|
71
|
+
def sign_out(self) -> None:
|
|
72
|
+
self.auth.sign_out()
|
|
73
|
+
self.token = None
|
|
74
|
+
|
|
75
|
+
def request(
|
|
76
|
+
self,
|
|
77
|
+
method: str,
|
|
78
|
+
path: str,
|
|
79
|
+
result_type: type[Result] | Any = None,
|
|
80
|
+
*,
|
|
81
|
+
path_parameters: dict[str, Any] | None = None,
|
|
82
|
+
query: dict[str, Any] | None = None,
|
|
83
|
+
body: BaseModel | dict[str, Any] | None = None,
|
|
84
|
+
) -> Any:
|
|
85
|
+
headers = {"accept": "application/json"}
|
|
86
|
+
token = self.token() if callable(self.token) else self.token
|
|
87
|
+
if token:
|
|
88
|
+
headers["authorization"] = f"Bearer {token}"
|
|
89
|
+
filled_path = path.format(**{name: quote(str(value), safe="") for name, value in (path_parameters or {}).items()})
|
|
90
|
+
content = None
|
|
91
|
+
if body is not None:
|
|
92
|
+
headers["content-type"] = "application/json"
|
|
93
|
+
payload = body.model_dump(mode="json", exclude_unset=True) if isinstance(body, BaseModel) else body
|
|
94
|
+
content = json.dumps(payload)
|
|
95
|
+
response = self.http_client.request(
|
|
96
|
+
method,
|
|
97
|
+
filled_path,
|
|
98
|
+
params={name: value for name, value in (query or {}).items() if value is not None},
|
|
99
|
+
headers=headers,
|
|
100
|
+
content=content,
|
|
101
|
+
)
|
|
102
|
+
if response.is_error:
|
|
103
|
+
raise GenticHubError(response.status_code, self.error_detail(response))
|
|
104
|
+
if response.status_code == 204 or result_type is None:
|
|
105
|
+
return None
|
|
106
|
+
return adapter_for(result_type).validate_python(response.json())
|
|
107
|
+
|
|
108
|
+
@staticmethod
|
|
109
|
+
def error_detail(response: httpx.Response) -> str:
|
|
110
|
+
try:
|
|
111
|
+
detail = response.json().get("detail")
|
|
112
|
+
except (ValueError, AttributeError):
|
|
113
|
+
detail = None
|
|
114
|
+
if detail is None:
|
|
115
|
+
return response.reason_phrase
|
|
116
|
+
return detail if isinstance(detail, str) else json.dumps(detail)
|