logion-client 0.1.0__py3-none-any.whl

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 (45) hide show
  1. logion/__init__.py +32 -0
  2. logion/_client.py +61 -0
  3. logion/_config.py +56 -0
  4. logion/_errors.py +74 -0
  5. logion/_http.py +187 -0
  6. logion/_types/__init__.py +2 -0
  7. logion/_version.py +19 -0
  8. logion/_versioning.py +22 -0
  9. logion/py.typed +0 -0
  10. logion/v1/__init__.py +36 -0
  11. logion/v1/_generated/__init__.py +2 -0
  12. logion/v1/_generated/operations.py +1085 -0
  13. logion/v1/_operation_map.py +90 -0
  14. logion/v1/_resources/__init__.py +2 -0
  15. logion/v1/_resources/admin/__init__.py +24 -0
  16. logion/v1/_resources/admin/courses.py +51 -0
  17. logion/v1/_resources/admin/payments.py +2 -0
  18. logion/v1/_resources/admin/reports.py +74 -0
  19. logion/v1/_resources/admin/shared.py +12 -0
  20. logion/v1/_resources/admin/users_agents.py +78 -0
  21. logion/v1/_resources/bounties/__init__.py +19 -0
  22. logion/v1/_resources/bounties/core.py +97 -0
  23. logion/v1/_resources/bounties/shared.py +14 -0
  24. logion/v1/_resources/bounties/submissions.py +108 -0
  25. logion/v1/_resources/course_reviews.py +141 -0
  26. logion/v1/_resources/courses/__init__.py +24 -0
  27. logion/v1/_resources/courses/core.py +181 -0
  28. logion/v1/_resources/courses/publication.py +40 -0
  29. logion/v1/_resources/courses/reviews.py +99 -0
  30. logion/v1/_resources/courses/shared.py +34 -0
  31. logion/v1/_resources/credits.py +40 -0
  32. logion/v1/_resources/health.py +24 -0
  33. logion/v1/_resources/identity.py +113 -0
  34. logion/v1/_resources/listings.py +77 -0
  35. logion/v1/_resources/notifications.py +53 -0
  36. logion/v1/_resources/payments.py +80 -0
  37. logion/v1/_resources/referrals.py +38 -0
  38. logion/v1/_resources/reports.py +72 -0
  39. logion/v1/_types/__init__.py +6 -0
  40. logion/v1/_types/generated/__init__.py +7 -0
  41. logion/v1/_types/generated/v1.py +2026 -0
  42. logion_client-0.1.0.dist-info/METADATA +194 -0
  43. logion_client-0.1.0.dist-info/RECORD +45 -0
  44. logion_client-0.1.0.dist-info/WHEEL +4 -0
  45. logion_client-0.1.0.dist-info/licenses/LICENSE +21 -0
logion/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Logion Python client SDK."""
3
+
4
+ from logion._client import LogionClient
5
+ from logion._errors import (
6
+ APIError,
7
+ AuthenticationError,
8
+ ClientError,
9
+ ConflictError,
10
+ ForbiddenError,
11
+ LogionError,
12
+ NotFoundError,
13
+ RateLimitError,
14
+ ServerError,
15
+ TransportError,
16
+ ValidationError,
17
+ )
18
+
19
+ __all__ = [
20
+ "APIError",
21
+ "AuthenticationError",
22
+ "ClientError",
23
+ "ConflictError",
24
+ "ForbiddenError",
25
+ "LogionClient",
26
+ "LogionError",
27
+ "NotFoundError",
28
+ "RateLimitError",
29
+ "ServerError",
30
+ "TransportError",
31
+ "ValidationError",
32
+ ]
logion/_client.py ADDED
@@ -0,0 +1,61 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Logion client — entry point for the SDK."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from logion._config import resolve_config
7
+ from logion._http import HttpClient
8
+ from logion._versioning import VersionedNamespaces
9
+ from logion.v1 import V1Namespace
10
+
11
+
12
+ class LogionClient:
13
+ """Python client for the Logion API.
14
+
15
+ Configuration can be provided via constructor arguments or
16
+ environment variables (``LOGION_API_KEY``, ``LOGION_BASE_URL``).
17
+ Omitted arguments fall back to the corresponding environment
18
+ variable, then to built-in defaults.
19
+
20
+ Usage::
21
+
22
+ from logion import LogionClient
23
+
24
+ client = LogionClient(api_key="lgk_...")
25
+ client.v1.health.check()
26
+ client.v1.listings.search(query="rag")
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ api_key: str | None = None,
33
+ base_url: str | None = None,
34
+ timeout: float | None = None,
35
+ max_retries: int | None = None,
36
+ extra_headers: dict[str, str] | None = None,
37
+ ) -> None:
38
+ config = resolve_config(
39
+ api_key=api_key,
40
+ base_url=base_url,
41
+ timeout=timeout,
42
+ max_retries=max_retries,
43
+ extra_headers=extra_headers,
44
+ )
45
+ self._http = HttpClient(config)
46
+ self._namespaces = VersionedNamespaces(self._http)
47
+
48
+ @property
49
+ def v1(self) -> V1Namespace:
50
+ """Access the v1 API namespace."""
51
+ return self._namespaces.v1
52
+
53
+ def close(self) -> None:
54
+ """Close the underlying HTTP connection."""
55
+ self._http.close()
56
+
57
+ def __enter__(self) -> LogionClient:
58
+ return self
59
+
60
+ def __exit__(self, *args: object) -> None:
61
+ self.close()
logion/_config.py ADDED
@@ -0,0 +1,56 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Client configuration."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ from dataclasses import dataclass, field
8
+
9
+ DEFAULT_BASE_URL = "https://api.logion.sh"
10
+ DEFAULT_TIMEOUT = 30.0
11
+ DEFAULT_MAX_RETRIES = 3
12
+
13
+
14
+ def resolve_env_or(key: str, default: str) -> str:
15
+ """Return *os.environ[key]* if set, otherwise *default*."""
16
+ return os.environ.get(key, default)
17
+
18
+
19
+ @dataclass
20
+ class ClientConfig:
21
+ """Resolved configuration for the Logion HTTP client."""
22
+
23
+ base_url: str = DEFAULT_BASE_URL
24
+ api_key: str = ""
25
+ timeout: float = DEFAULT_TIMEOUT
26
+ max_retries: int = DEFAULT_MAX_RETRIES
27
+ extra_headers: dict[str, str] = field(default_factory=dict)
28
+
29
+
30
+ def resolve_config(
31
+ *,
32
+ api_key: str | None = None,
33
+ base_url: str | None = None,
34
+ timeout: float | None = None,
35
+ max_retries: int | None = None,
36
+ extra_headers: dict[str, str] | None = None,
37
+ ) -> ClientConfig:
38
+ """Resolve SDK configuration from explicit values, env vars, or
39
+ defaults — in that priority order."""
40
+ return ClientConfig(
41
+ base_url=(
42
+ base_url
43
+ if base_url is not None
44
+ else resolve_env_or("LOGION_BASE_URL", DEFAULT_BASE_URL)
45
+ ),
46
+ api_key=(
47
+ api_key
48
+ if api_key is not None
49
+ else resolve_env_or("LOGION_API_KEY", "")
50
+ ),
51
+ timeout=timeout if timeout is not None else DEFAULT_TIMEOUT,
52
+ max_retries=(
53
+ max_retries if max_retries is not None else DEFAULT_MAX_RETRIES
54
+ ),
55
+ extra_headers=extra_headers if extra_headers is not None else {},
56
+ )
logion/_errors.py ADDED
@@ -0,0 +1,74 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Error hierarchy for the Logion SDK."""
3
+
4
+ from __future__ import annotations
5
+
6
+
7
+ class LogionError(Exception):
8
+ """Base exception for all SDK errors."""
9
+
10
+
11
+ class APIError(LogionError):
12
+ """Error returned by the Logion API."""
13
+
14
+ def __init__(
15
+ self,
16
+ status_code: int,
17
+ detail: str | list[dict[str, object]],
18
+ request_id: str | None = None,
19
+ ) -> None:
20
+ self.status_code = status_code
21
+ self.detail = detail
22
+ self.request_id = request_id
23
+ msg = f"{status_code}"
24
+ if request_id:
25
+ msg += f" (request_id={request_id})"
26
+ msg += f": {detail}"
27
+ super().__init__(msg)
28
+
29
+
30
+ class ClientError(APIError):
31
+ """4xx response that isn't mapped to a specific error class."""
32
+
33
+
34
+ class AuthenticationError(APIError):
35
+ """401 — Invalid or missing API key."""
36
+
37
+
38
+ class ForbiddenError(APIError):
39
+ """403 — Insufficient permissions."""
40
+
41
+
42
+ class NotFoundError(APIError):
43
+ """404 — Resource not found."""
44
+
45
+
46
+ class ConflictError(APIError):
47
+ """409 — Resource already exists."""
48
+
49
+
50
+ class ValidationError(APIError):
51
+ """422 — Request body or parameters invalid."""
52
+
53
+
54
+ class RateLimitError(APIError):
55
+ """429 — Too many requests."""
56
+
57
+
58
+ class ServerError(APIError):
59
+ """5xx — Server-side error."""
60
+
61
+
62
+ class TransportError(LogionError):
63
+ """Network-level failure (DNS, connection, timeout, etc.).
64
+
65
+ Wraps an ``httpx.TransportError`` after retries are exhausted.
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ message: str,
71
+ original: Exception | None = None,
72
+ ) -> None:
73
+ self.original = original
74
+ super().__init__(message)
logion/_http.py ADDED
@@ -0,0 +1,187 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """HTTP transport wrapper with retry and auth."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import time
7
+ import uuid
8
+ from typing import Any, TypeVar, cast
9
+
10
+ import httpx
11
+ from pydantic import BaseModel
12
+
13
+ from logion._config import ClientConfig
14
+ from logion._errors import (
15
+ APIError,
16
+ AuthenticationError,
17
+ ClientError,
18
+ ConflictError,
19
+ ForbiddenError,
20
+ NotFoundError,
21
+ RateLimitError,
22
+ ServerError,
23
+ TransportError,
24
+ ValidationError,
25
+ )
26
+
27
+ _RETRYABLE_STATUS_CODES = {429, 502, 503, 504}
28
+ _RETRYABLE_METHODS = {"GET", "HEAD", "OPTIONS"}
29
+ _STATUS_ERROR_MAP: dict[int, type[APIError]] = {
30
+ 401: AuthenticationError,
31
+ 403: ForbiddenError,
32
+ 404: NotFoundError,
33
+ 409: ConflictError,
34
+ 422: ValidationError,
35
+ 429: RateLimitError,
36
+ }
37
+
38
+ T = TypeVar("T", bound=BaseModel)
39
+
40
+
41
+ def _raise_for_status(response: httpx.Response) -> None:
42
+ """Raise a typed SDK error for non-2xx responses."""
43
+ if response.is_success:
44
+ return
45
+
46
+ status_code = response.status_code
47
+ request_id: str | None = response.headers.get("x-request-id")
48
+ detail: str | list[dict[str, object]] = response.text
49
+
50
+ try:
51
+ body = response.json()
52
+ detail = body.get("detail", detail)
53
+ except Exception: # nosec B110 — intentional: non-JSON responses are fine as plain text
54
+ pass
55
+
56
+ error_cls = _STATUS_ERROR_MAP.get(status_code)
57
+ if error_cls is None:
58
+ error_cls = ServerError if status_code >= 500 else ClientError
59
+
60
+ raise error_cls(
61
+ status_code=status_code,
62
+ detail=detail,
63
+ request_id=request_id,
64
+ )
65
+
66
+
67
+ class HttpClient:
68
+ """Wraps httpx.Client with retry, auth, and error mapping."""
69
+
70
+ def __init__(self, config: ClientConfig) -> None:
71
+ self._config = config
72
+ self._client = httpx.Client(
73
+ base_url=config.base_url,
74
+ timeout=config.timeout,
75
+ headers=self._build_headers(config),
76
+ )
77
+
78
+ @staticmethod
79
+ def _build_headers(config: ClientConfig) -> dict[str, str]:
80
+ headers: dict[str, str] = {
81
+ "Accept": "application/json",
82
+ **config.extra_headers,
83
+ }
84
+ if config.api_key:
85
+ headers["Authorization"] = f"Bearer {config.api_key}"
86
+ return headers
87
+
88
+ def request(
89
+ self,
90
+ method: str,
91
+ path: str,
92
+ *,
93
+ params: dict[str, Any] | None = None,
94
+ json: dict[str, Any] | None = None,
95
+ ) -> dict[str, Any]:
96
+ """Send a request and return raw JSON dict."""
97
+ if self._config.max_retries < 0:
98
+ msg = f"max_retries must be >= 0, got {self._config.max_retries}"
99
+ raise ValueError(msg)
100
+ can_retry = method.upper() in _RETRYABLE_METHODS
101
+ last_exc: httpx.TransportError | None = None
102
+ max_attempts = self._config.max_retries + 1 if can_retry else 1
103
+
104
+ for attempt in range(max_attempts):
105
+ request_id = str(uuid.uuid4())
106
+ headers = {"X-Request-ID": request_id}
107
+
108
+ try:
109
+ response = self._client.request(
110
+ method,
111
+ path,
112
+ params=params,
113
+ json=json,
114
+ headers=headers,
115
+ )
116
+ except httpx.TransportError as exc:
117
+ last_exc = exc
118
+ if can_retry and attempt < max_attempts - 1:
119
+ backoff = 0.5 * (2**attempt)
120
+ time.sleep(backoff)
121
+ continue
122
+ raise TransportError(
123
+ f"Request failed after {attempt + 1} attempt(s): {exc}",
124
+ original=exc,
125
+ ) from exc
126
+
127
+ if (
128
+ response.status_code in _RETRYABLE_STATUS_CODES
129
+ and can_retry
130
+ and attempt < max_attempts - 1
131
+ ):
132
+ backoff = 0.5 * (2**attempt)
133
+ time.sleep(backoff)
134
+ continue
135
+
136
+ _raise_for_status(response)
137
+ return response.json()
138
+
139
+ assert last_exc is not None # for type checker
140
+ raise TransportError(
141
+ f"Request failed after {max_attempts} attempts",
142
+ original=last_exc,
143
+ ) from last_exc
144
+
145
+ def request_model(
146
+ self,
147
+ method: str,
148
+ path: str,
149
+ model_type: type[T],
150
+ *,
151
+ params: dict[str, Any] | None = None,
152
+ json: dict[str, Any] | None = None,
153
+ ) -> T:
154
+ """Send a request and parse the response into a Pydantic model."""
155
+ data = self.request(
156
+ method,
157
+ path,
158
+ params=params,
159
+ json=json,
160
+ )
161
+ return cast(T, model_type.model_validate(data))
162
+
163
+ def request_list(
164
+ self,
165
+ method: str,
166
+ path: str,
167
+ *,
168
+ params: dict[str, Any] | None = None,
169
+ json: dict[str, Any] | None = None,
170
+ ) -> list[dict[str, Any]]:
171
+ """Send a request and return raw JSON as a list of dicts.
172
+
173
+ Use this for endpoints whose OpenAPI contract defines an array
174
+ response (``[...]``) rather than a JSON object.
175
+ """
176
+ data = self.request(method, path, params=params, json=json)
177
+ if not isinstance(data, list):
178
+ msg = (
179
+ f"Expected a JSON array from "
180
+ f"{method} {path}, got {type(data).__name__}"
181
+ )
182
+ raise TypeError(msg)
183
+ return data
184
+
185
+ def close(self) -> None:
186
+ """Close the underlying httpx client."""
187
+ self._client.close()
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Shared SDK types."""
logion/_version.py ADDED
@@ -0,0 +1,19 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Runtime-accessible package version."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from importlib.metadata import PackageNotFoundError
7
+ from importlib.metadata import version as _pkg_version
8
+
9
+ try:
10
+ __version__: str = _pkg_version("logion-client")
11
+ except PackageNotFoundError: # local checkout / sdist
12
+ import tomllib
13
+ from pathlib import Path
14
+
15
+ _pyproject = Path(__file__).resolve().parents[2] / "pyproject.toml"
16
+ with _pyproject.open("rb") as _f:
17
+ __version__ = tomllib.load(_f)["project"]["version"]
18
+
19
+ __all__ = ["__version__"]
logion/_versioning.py ADDED
@@ -0,0 +1,22 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Versioned namespace factory."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from logion._http import HttpClient
7
+ from logion.v1 import V1Namespace
8
+
9
+
10
+ class VersionedNamespaces:
11
+ """Lazy namespace accessors for each API version."""
12
+
13
+ def __init__(self, http: HttpClient) -> None:
14
+ self._http = http
15
+ self._v1: V1Namespace | None = None
16
+
17
+ @property
18
+ def v1(self) -> V1Namespace:
19
+ """Access the v1 API namespace."""
20
+ if self._v1 is None:
21
+ self._v1 = V1Namespace(self._http)
22
+ return self._v1
logion/py.typed ADDED
File without changes
logion/v1/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """V1 API namespace."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from logion._http import HttpClient
7
+ from logion.v1._resources.admin import AdminResource
8
+ from logion.v1._resources.bounties import BountiesResource
9
+ from logion.v1._resources.course_reviews import CourseReviewsResource
10
+ from logion.v1._resources.courses import CoursesResource
11
+ from logion.v1._resources.credits import CreditsResource
12
+ from logion.v1._resources.health import HealthResource
13
+ from logion.v1._resources.identity import IdentityResource
14
+ from logion.v1._resources.listings import ListingsResource
15
+ from logion.v1._resources.notifications import NotificationsResource
16
+ from logion.v1._resources.payments import PaymentsResource
17
+ from logion.v1._resources.referrals import ReferralsResource
18
+ from logion.v1._resources.reports import ReportsResource
19
+
20
+
21
+ class V1Namespace:
22
+ """Namespace for v1 API endpoints."""
23
+
24
+ def __init__(self, http: HttpClient) -> None:
25
+ self.health = HealthResource(http)
26
+ self.identity = IdentityResource(http)
27
+ self.listings = ListingsResource(http)
28
+ self.courses = CoursesResource(http)
29
+ self.credits = CreditsResource(http)
30
+ self.payments = PaymentsResource(http)
31
+ self.course_reviews = CourseReviewsResource(http)
32
+ self.notifications = NotificationsResource(http)
33
+ self.reports = ReportsResource(http)
34
+ self.admin = AdminResource(http)
35
+ self.bounties = BountiesResource(http)
36
+ self.referrals = ReferralsResource(http)
@@ -0,0 +1,2 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """Generated v1 API internals."""