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.
@@ -0,0 +1,4 @@
1
+ .venv
2
+ dist
3
+ __pycache__
4
+ *.egg-info
@@ -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,4 @@
1
+ from gentic_sdk.client import GenticHub
2
+ from gentic_sdk.exceptions import GenticHubError
3
+
4
+ __all__ = ["GenticHub", "GenticHubError"]
@@ -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)
@@ -0,0 +1,5 @@
1
+ class GenticHubError(Exception):
2
+ def __init__(self, status: int, detail: str) -> None:
3
+ super().__init__(detail)
4
+ self.status = status
5
+ self.detail = detail