freeastroapi 0.2.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.
- freeastroapi/__init__.py +38 -0
- freeastroapi/_generated.py +4455 -0
- freeastroapi/client.py +309 -0
- freeastroapi/errors.py +123 -0
- freeastroapi/models.py +221 -0
- freeastroapi/py.typed +1 -0
- freeastroapi/resources.py +190 -0
- freeastroapi/schemas.py +7664 -0
- freeastroapi-0.2.0.dist-info/METADATA +158 -0
- freeastroapi-0.2.0.dist-info/RECORD +12 -0
- freeastroapi-0.2.0.dist-info/WHEEL +4 -0
- freeastroapi-0.2.0.dist-info/licenses/LICENSE +201 -0
freeastroapi/client.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from email.utils import parsedate_to_datetime
|
|
9
|
+
from typing import Any, Generic, TypeVar
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from ._generated import AsyncGeneratedResources, GeneratedResources
|
|
14
|
+
from .errors import NetworkError, TimeoutError, error_from_response
|
|
15
|
+
from .resources import (
|
|
16
|
+
AsyncGeoResource,
|
|
17
|
+
AsyncMoonResource,
|
|
18
|
+
AsyncSkyEventsResource,
|
|
19
|
+
AsyncWesternResource,
|
|
20
|
+
GeoResource,
|
|
21
|
+
MoonResource,
|
|
22
|
+
SkyEventsResource,
|
|
23
|
+
WesternResource,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "https://api.freeastroapi.com"
|
|
29
|
+
DEFAULT_RETRY_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
|
|
30
|
+
IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "OPTIONS"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class RetryConfig:
|
|
35
|
+
retries: int = 2
|
|
36
|
+
base_delay: float = 0.25
|
|
37
|
+
max_delay: float = 5.0
|
|
38
|
+
statuses: frozenset[int] = DEFAULT_RETRY_STATUSES
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
DEFAULT_RETRY = RetryConfig()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class RequestOptions:
|
|
46
|
+
headers: Mapping[str, str] = field(default_factory=dict)
|
|
47
|
+
idempotency_key: str | None = None
|
|
48
|
+
timeout: float | None = None
|
|
49
|
+
retry: bool | RetryConfig | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True, slots=True)
|
|
53
|
+
class SDKResponse(Generic[T]):
|
|
54
|
+
data: T
|
|
55
|
+
response: httpx.Response
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _retry_after_seconds(response: httpx.Response) -> float | None:
|
|
59
|
+
value = response.headers.get("retry-after")
|
|
60
|
+
if not value:
|
|
61
|
+
return None
|
|
62
|
+
try:
|
|
63
|
+
return max(0.0, float(value))
|
|
64
|
+
except ValueError:
|
|
65
|
+
try:
|
|
66
|
+
retry_at = parsedate_to_datetime(value)
|
|
67
|
+
if retry_at.tzinfo is None:
|
|
68
|
+
retry_at = retry_at.replace(tzinfo=UTC)
|
|
69
|
+
return max(0.0, (retry_at - datetime.now(UTC)).total_seconds())
|
|
70
|
+
except (TypeError, ValueError, OverflowError):
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _response_data(response: httpx.Response) -> Any:
|
|
75
|
+
if response.status_code == 204:
|
|
76
|
+
return None
|
|
77
|
+
content_type = response.headers.get("content-type", "")
|
|
78
|
+
if "application/json" in content_type or "+json" in content_type:
|
|
79
|
+
return response.json()
|
|
80
|
+
return response.text
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class _ClientSettings:
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
*,
|
|
87
|
+
api_key: str,
|
|
88
|
+
base_url: str,
|
|
89
|
+
timeout: float,
|
|
90
|
+
retry: RetryConfig | None,
|
|
91
|
+
headers: Mapping[str, str] | None,
|
|
92
|
+
) -> None:
|
|
93
|
+
if not api_key.strip():
|
|
94
|
+
raise ValueError("api_key is required")
|
|
95
|
+
self.api_key = api_key
|
|
96
|
+
self.base_url = base_url.rstrip("/")
|
|
97
|
+
self.timeout = timeout
|
|
98
|
+
self.retry = retry
|
|
99
|
+
self.headers = dict(headers or {})
|
|
100
|
+
|
|
101
|
+
def request_headers(self, options: RequestOptions) -> dict[str, str]:
|
|
102
|
+
headers = {key.lower(): value for key, value in {**self.headers, **options.headers}.items()}
|
|
103
|
+
headers.update({"accept": "application/json", "x-api-key": self.api_key})
|
|
104
|
+
if options.idempotency_key:
|
|
105
|
+
headers["idempotency-key"] = options.idempotency_key
|
|
106
|
+
return headers
|
|
107
|
+
|
|
108
|
+
def retry_config(self, method: str, options: RequestOptions) -> RetryConfig | None:
|
|
109
|
+
if self.retry is None or options.retry is False:
|
|
110
|
+
return None
|
|
111
|
+
explicitly_enabled = options.retry is True or isinstance(options.retry, RetryConfig)
|
|
112
|
+
replay_safe = method in IDEMPOTENT_METHODS or bool(options.idempotency_key)
|
|
113
|
+
if not explicitly_enabled and not replay_safe:
|
|
114
|
+
return None
|
|
115
|
+
return options.retry if isinstance(options.retry, RetryConfig) else self.retry
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class FreeAstro(GeneratedResources):
|
|
119
|
+
"""FreeAstroAPI client.
|
|
120
|
+
|
|
121
|
+
Resources follow the API paths: ``client.western.natal.insights(...)`` calls
|
|
122
|
+
``POST /api/v1/western/natal/insights``. Where an operation exists in several API versions,
|
|
123
|
+
the plain method name calls the latest one and older ones carry a suffix
|
|
124
|
+
(``client.vedic.chart`` is v2, ``client.vedic.chart_v1`` is v1).
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
api_key: str,
|
|
131
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
132
|
+
timeout: float = 20.0,
|
|
133
|
+
retry: RetryConfig | None = DEFAULT_RETRY,
|
|
134
|
+
headers: Mapping[str, str] | None = None,
|
|
135
|
+
transport: httpx.BaseTransport | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
self._settings = _ClientSettings(
|
|
138
|
+
api_key=api_key,
|
|
139
|
+
base_url=base_url,
|
|
140
|
+
timeout=timeout,
|
|
141
|
+
retry=retry,
|
|
142
|
+
headers=headers,
|
|
143
|
+
)
|
|
144
|
+
self._http = httpx.Client(base_url=self._settings.base_url, transport=transport)
|
|
145
|
+
self.geo = GeoResource(self)
|
|
146
|
+
self.moon = MoonResource(self)
|
|
147
|
+
self.sky_events = SkyEventsResource(self)
|
|
148
|
+
self.western = WesternResource(self)
|
|
149
|
+
self._init_generated_resources(self)
|
|
150
|
+
|
|
151
|
+
def __enter__(self) -> FreeAstro:
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, *_: object) -> None:
|
|
155
|
+
self.close()
|
|
156
|
+
|
|
157
|
+
def close(self) -> None:
|
|
158
|
+
self._http.close()
|
|
159
|
+
|
|
160
|
+
def request(
|
|
161
|
+
self,
|
|
162
|
+
method: str,
|
|
163
|
+
path: str,
|
|
164
|
+
*,
|
|
165
|
+
params: Mapping[str, Any] | None = None,
|
|
166
|
+
json: Any = None,
|
|
167
|
+
options: RequestOptions | None = None,
|
|
168
|
+
) -> Any:
|
|
169
|
+
return self.request_raw(method, path, params=params, json=json, options=options).data
|
|
170
|
+
|
|
171
|
+
def request_raw(
|
|
172
|
+
self,
|
|
173
|
+
method: str,
|
|
174
|
+
path: str,
|
|
175
|
+
*,
|
|
176
|
+
params: Mapping[str, Any] | None = None,
|
|
177
|
+
json: Any = None,
|
|
178
|
+
options: RequestOptions | None = None,
|
|
179
|
+
) -> SDKResponse[Any]:
|
|
180
|
+
options = options or RequestOptions()
|
|
181
|
+
method = method.upper()
|
|
182
|
+
retry = self._settings.retry_config(method, options)
|
|
183
|
+
attempt = 0
|
|
184
|
+
|
|
185
|
+
while True:
|
|
186
|
+
try:
|
|
187
|
+
response = self._http.request(
|
|
188
|
+
method,
|
|
189
|
+
path,
|
|
190
|
+
params=params,
|
|
191
|
+
json=json,
|
|
192
|
+
headers=self._settings.request_headers(options),
|
|
193
|
+
timeout=options.timeout if options.timeout is not None else self._settings.timeout,
|
|
194
|
+
)
|
|
195
|
+
except httpx.TimeoutException as exc:
|
|
196
|
+
if retry and attempt < retry.retries:
|
|
197
|
+
time.sleep(min(retry.base_delay * 2**attempt, retry.max_delay))
|
|
198
|
+
attempt += 1
|
|
199
|
+
continue
|
|
200
|
+
raise TimeoutError("FreeAstroAPI request timed out") from exc
|
|
201
|
+
except httpx.HTTPError as exc:
|
|
202
|
+
if retry and attempt < retry.retries:
|
|
203
|
+
time.sleep(min(retry.base_delay * 2**attempt, retry.max_delay))
|
|
204
|
+
attempt += 1
|
|
205
|
+
continue
|
|
206
|
+
raise NetworkError("Unable to reach FreeAstroAPI") from exc
|
|
207
|
+
|
|
208
|
+
if response.is_success:
|
|
209
|
+
return SDKResponse(data=_response_data(response), response=response)
|
|
210
|
+
if retry and response.status_code in retry.statuses and attempt < retry.retries:
|
|
211
|
+
delay = _retry_after_seconds(response)
|
|
212
|
+
time.sleep(min(delay if delay is not None else retry.base_delay * 2**attempt, retry.max_delay))
|
|
213
|
+
attempt += 1
|
|
214
|
+
continue
|
|
215
|
+
raise error_from_response(response)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class AsyncFreeAstro(AsyncGeneratedResources):
|
|
219
|
+
"""Async FreeAstroAPI client with the same resources as :class:`FreeAstro`."""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
*,
|
|
224
|
+
api_key: str,
|
|
225
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
226
|
+
timeout: float = 20.0,
|
|
227
|
+
retry: RetryConfig | None = DEFAULT_RETRY,
|
|
228
|
+
headers: Mapping[str, str] | None = None,
|
|
229
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
230
|
+
) -> None:
|
|
231
|
+
self._settings = _ClientSettings(
|
|
232
|
+
api_key=api_key,
|
|
233
|
+
base_url=base_url,
|
|
234
|
+
timeout=timeout,
|
|
235
|
+
retry=retry,
|
|
236
|
+
headers=headers,
|
|
237
|
+
)
|
|
238
|
+
self._http = httpx.AsyncClient(base_url=self._settings.base_url, transport=transport)
|
|
239
|
+
self.geo = AsyncGeoResource(self)
|
|
240
|
+
self.moon = AsyncMoonResource(self)
|
|
241
|
+
self.sky_events = AsyncSkyEventsResource(self)
|
|
242
|
+
self.western = AsyncWesternResource(self)
|
|
243
|
+
self._init_generated_resources(self)
|
|
244
|
+
|
|
245
|
+
async def __aenter__(self) -> AsyncFreeAstro:
|
|
246
|
+
return self
|
|
247
|
+
|
|
248
|
+
async def __aexit__(self, *_: object) -> None:
|
|
249
|
+
await self.close()
|
|
250
|
+
|
|
251
|
+
async def close(self) -> None:
|
|
252
|
+
await self._http.aclose()
|
|
253
|
+
|
|
254
|
+
async def request(
|
|
255
|
+
self,
|
|
256
|
+
method: str,
|
|
257
|
+
path: str,
|
|
258
|
+
*,
|
|
259
|
+
params: Mapping[str, Any] | None = None,
|
|
260
|
+
json: Any = None,
|
|
261
|
+
options: RequestOptions | None = None,
|
|
262
|
+
) -> Any:
|
|
263
|
+
return (await self.request_raw(method, path, params=params, json=json, options=options)).data
|
|
264
|
+
|
|
265
|
+
async def request_raw(
|
|
266
|
+
self,
|
|
267
|
+
method: str,
|
|
268
|
+
path: str,
|
|
269
|
+
*,
|
|
270
|
+
params: Mapping[str, Any] | None = None,
|
|
271
|
+
json: Any = None,
|
|
272
|
+
options: RequestOptions | None = None,
|
|
273
|
+
) -> SDKResponse[Any]:
|
|
274
|
+
options = options or RequestOptions()
|
|
275
|
+
method = method.upper()
|
|
276
|
+
retry = self._settings.retry_config(method, options)
|
|
277
|
+
attempt = 0
|
|
278
|
+
|
|
279
|
+
while True:
|
|
280
|
+
try:
|
|
281
|
+
response = await self._http.request(
|
|
282
|
+
method,
|
|
283
|
+
path,
|
|
284
|
+
params=params,
|
|
285
|
+
json=json,
|
|
286
|
+
headers=self._settings.request_headers(options),
|
|
287
|
+
timeout=options.timeout if options.timeout is not None else self._settings.timeout,
|
|
288
|
+
)
|
|
289
|
+
except httpx.TimeoutException as exc:
|
|
290
|
+
if retry and attempt < retry.retries:
|
|
291
|
+
await asyncio.sleep(min(retry.base_delay * 2**attempt, retry.max_delay))
|
|
292
|
+
attempt += 1
|
|
293
|
+
continue
|
|
294
|
+
raise TimeoutError("FreeAstroAPI request timed out") from exc
|
|
295
|
+
except httpx.HTTPError as exc:
|
|
296
|
+
if retry and attempt < retry.retries:
|
|
297
|
+
await asyncio.sleep(min(retry.base_delay * 2**attempt, retry.max_delay))
|
|
298
|
+
attempt += 1
|
|
299
|
+
continue
|
|
300
|
+
raise NetworkError("Unable to reach FreeAstroAPI") from exc
|
|
301
|
+
|
|
302
|
+
if response.is_success:
|
|
303
|
+
return SDKResponse(data=_response_data(response), response=response)
|
|
304
|
+
if retry and response.status_code in retry.statuses and attempt < retry.retries:
|
|
305
|
+
delay = _retry_after_seconds(response)
|
|
306
|
+
await asyncio.sleep(min(delay if delay is not None else retry.base_delay * 2**attempt, retry.max_delay))
|
|
307
|
+
attempt += 1
|
|
308
|
+
continue
|
|
309
|
+
raise error_from_response(response)
|
freeastroapi/errors.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from email.utils import parsedate_to_datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FreeAstroError(Exception):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
*,
|
|
15
|
+
status: int | None = None,
|
|
16
|
+
code: str | None = None,
|
|
17
|
+
request_id: str | None = None,
|
|
18
|
+
details: Any = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status = status
|
|
22
|
+
self.code = code
|
|
23
|
+
self.request_id = request_id
|
|
24
|
+
self.details = details
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AuthenticationError(FreeAstroError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PermissionError(FreeAstroError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ValidationError(FreeAstroError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ConflictError(FreeAstroError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RateLimitError(FreeAstroError):
|
|
44
|
+
def __init__(self, message: str, *, retry_after: float | None = None, **kwargs: Any) -> None:
|
|
45
|
+
super().__init__(message, **kwargs)
|
|
46
|
+
self.retry_after = retry_after
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ServerError(FreeAstroError):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NetworkError(FreeAstroError):
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TimeoutError(NetworkError):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _retry_after_seconds(value: str | None) -> float | None:
|
|
62
|
+
if value is None:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
return max(0.0, float(value))
|
|
66
|
+
except ValueError:
|
|
67
|
+
try:
|
|
68
|
+
retry_at = parsedate_to_datetime(value)
|
|
69
|
+
if retry_at.tzinfo is None:
|
|
70
|
+
retry_at = retry_at.replace(tzinfo=UTC)
|
|
71
|
+
return max(0.0, (retry_at - datetime.now(UTC)).total_seconds())
|
|
72
|
+
except (TypeError, ValueError, OverflowError):
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def error_from_response(response: httpx.Response) -> FreeAstroError:
|
|
77
|
+
try:
|
|
78
|
+
payload: dict[str, Any] = response.json()
|
|
79
|
+
except (ValueError, TypeError):
|
|
80
|
+
payload = {}
|
|
81
|
+
|
|
82
|
+
detail = payload.get("detail")
|
|
83
|
+
message_value = (
|
|
84
|
+
detail
|
|
85
|
+
if isinstance(detail, str) and detail.strip()
|
|
86
|
+
else payload.get("message") or payload.get("error") or response.reason_phrase or f"HTTP {response.status_code}"
|
|
87
|
+
)
|
|
88
|
+
message = message_value if isinstance(message_value, str) else str(message_value)
|
|
89
|
+
code_value = payload.get("code")
|
|
90
|
+
code = code_value if isinstance(code_value, str) else None
|
|
91
|
+
request_id_value = response.headers.get("x-request-id") or payload.get("trace_id")
|
|
92
|
+
request_id = request_id_value if isinstance(request_id_value, str) else None
|
|
93
|
+
|
|
94
|
+
def error(error_type: type[FreeAstroError]) -> FreeAstroError:
|
|
95
|
+
return error_type(
|
|
96
|
+
message,
|
|
97
|
+
status=response.status_code,
|
|
98
|
+
code=code,
|
|
99
|
+
request_id=request_id,
|
|
100
|
+
details=payload,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if response.status_code == 401:
|
|
104
|
+
return error(AuthenticationError)
|
|
105
|
+
if response.status_code == 403:
|
|
106
|
+
return error(PermissionError)
|
|
107
|
+
if response.status_code in (400, 422):
|
|
108
|
+
return error(ValidationError)
|
|
109
|
+
if response.status_code == 409:
|
|
110
|
+
return error(ConflictError)
|
|
111
|
+
if response.status_code == 429:
|
|
112
|
+
retry_after = _retry_after_seconds(response.headers.get("retry-after"))
|
|
113
|
+
return RateLimitError(
|
|
114
|
+
message,
|
|
115
|
+
retry_after=retry_after,
|
|
116
|
+
status=response.status_code,
|
|
117
|
+
code=code,
|
|
118
|
+
request_id=request_id,
|
|
119
|
+
details=payload,
|
|
120
|
+
)
|
|
121
|
+
if response.status_code >= 500:
|
|
122
|
+
return error(ServerError)
|
|
123
|
+
return error(FreeAstroError)
|
freeastroapi/models.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RequestModel(BaseModel):
|
|
9
|
+
model_config = ConfigDict(extra="forbid")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ResponseModel(BaseModel):
|
|
13
|
+
# Additive API response fields remain forward compatible and are preserved.
|
|
14
|
+
model_config = ConfigDict(extra="allow")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GeoSearchParams(RequestModel):
|
|
18
|
+
q: str = Field(min_length=2, max_length=100)
|
|
19
|
+
limit: int = Field(default=10, ge=1, le=50)
|
|
20
|
+
country: str | None = Field(default=None, pattern=r"^[A-Z]{2}$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class City(ResponseModel):
|
|
24
|
+
name: str
|
|
25
|
+
country: str
|
|
26
|
+
state: str | None = None
|
|
27
|
+
district: str | None = None
|
|
28
|
+
lat: float
|
|
29
|
+
lng: float
|
|
30
|
+
timezone: str
|
|
31
|
+
population: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CitySearchResponse(ResponseModel):
|
|
35
|
+
results: list[City]
|
|
36
|
+
count: int
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class MoonLocationParams(RequestModel):
|
|
40
|
+
city: str | None = Field(default=None, min_length=1, max_length=200)
|
|
41
|
+
lat: float | None = Field(default=None, ge=-90, le=90)
|
|
42
|
+
lon: float | None = Field(default=None, ge=-180, le=180)
|
|
43
|
+
tz_str: str = "AUTO"
|
|
44
|
+
include_eclipse: bool = False
|
|
45
|
+
include_forecast: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MoonPhaseParams(MoonLocationParams):
|
|
49
|
+
date: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class MoonMonthParams(MoonLocationParams):
|
|
53
|
+
year: int = Field(ge=1, le=9999)
|
|
54
|
+
month: int = Field(ge=1, le=12)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
MoonPhaseName = Literal[
|
|
58
|
+
"New Moon",
|
|
59
|
+
"Waxing Crescent",
|
|
60
|
+
"First Quarter",
|
|
61
|
+
"Waxing Gibbous",
|
|
62
|
+
"Full Moon",
|
|
63
|
+
"Waning Gibbous",
|
|
64
|
+
"Last Quarter",
|
|
65
|
+
"Waning Crescent",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MoonPhaseData(ResponseModel):
|
|
70
|
+
name: MoonPhaseName
|
|
71
|
+
phase_angle_deg: float
|
|
72
|
+
illumination: float
|
|
73
|
+
age_days: float
|
|
74
|
+
distance_km: float
|
|
75
|
+
is_waxing: bool
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class MaximumLocation(ResponseModel):
|
|
79
|
+
lat: float
|
|
80
|
+
lon: float
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class EclipseData(ResponseModel):
|
|
84
|
+
is_eclipse: bool
|
|
85
|
+
is_blood_moon: bool
|
|
86
|
+
kind: Literal["solar", "lunar"] | None = None
|
|
87
|
+
type: Literal["total", "annular", "hybrid", "partial", "penumbral"] | None = None
|
|
88
|
+
date: str | None = None
|
|
89
|
+
starts_at: str | None = None
|
|
90
|
+
ends_at: str | None = None
|
|
91
|
+
central_phase_starts_at: str | None = None
|
|
92
|
+
central_phase_ends_at: str | None = None
|
|
93
|
+
visibility: str | None = None
|
|
94
|
+
visible_at_location: bool | None = None
|
|
95
|
+
type_at_location: Literal["total", "annular", "partial"] | None = None
|
|
96
|
+
days_from_query: float | None = None
|
|
97
|
+
magnitude: float | None = None
|
|
98
|
+
obscuration: float | None = Field(default=None, ge=0, le=1)
|
|
99
|
+
maximum_location: MaximumLocation | None = None
|
|
100
|
+
local_starts_at: str | None = None
|
|
101
|
+
local_maximum_at: str | None = None
|
|
102
|
+
local_ends_at: str | None = None
|
|
103
|
+
magnitude_at_location: float | None = None
|
|
104
|
+
obscuration_at_location: float | None = Field(default=None, ge=0, le=1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class NextMoonPhases(ResponseModel):
|
|
108
|
+
new_moon: str
|
|
109
|
+
first_quarter: str
|
|
110
|
+
full_moon: str
|
|
111
|
+
last_quarter: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class MoonPhaseResponse(ResponseModel):
|
|
115
|
+
timestamp: str
|
|
116
|
+
phase: MoonPhaseData
|
|
117
|
+
eclipse: EclipseData | None = None
|
|
118
|
+
next_phases: NextMoonPhases
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class MoonMonthDay(MoonPhaseResponse):
|
|
122
|
+
calendar_date: str
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class MoonMonthResponse(ResponseModel):
|
|
126
|
+
year: int
|
|
127
|
+
month: int
|
|
128
|
+
month_name: str
|
|
129
|
+
days_in_month: int
|
|
130
|
+
calendar_basis: Literal["local", "utc"]
|
|
131
|
+
calendar_timezone: str
|
|
132
|
+
days: list[MoonMonthDay]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class SkyEventsParams(RequestModel):
|
|
136
|
+
date: str
|
|
137
|
+
timezone: str = "UTC"
|
|
138
|
+
include_moon: bool = False
|
|
139
|
+
include_exact_hits: bool = True
|
|
140
|
+
include_moon_exact_hits: bool = False
|
|
141
|
+
include_ingresses: bool = True
|
|
142
|
+
include_retrogrades: bool = False
|
|
143
|
+
include_lunations: bool = False
|
|
144
|
+
include_formulation: bool = False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class SkyEvent(ResponseModel):
|
|
148
|
+
id: str
|
|
149
|
+
type: Literal["moon_ingress", "planet_ingress", "exact_aspect", "retrograde_station", "lunation"]
|
|
150
|
+
title: str
|
|
151
|
+
formulation: str | None = None
|
|
152
|
+
exact_time: str
|
|
153
|
+
importance: Literal["daily", "medium", "major"]
|
|
154
|
+
body: str | None = None
|
|
155
|
+
sign: str | None = None
|
|
156
|
+
bodies: list[str] | None = None
|
|
157
|
+
aspect: Literal["conjunction", "sextile", "square", "trine", "opposition"] | None = None
|
|
158
|
+
angle: Literal[0, 60, 90, 120, 180] | None = None
|
|
159
|
+
orb: int | None = None
|
|
160
|
+
direction: Literal["retrograde", "direct"] | None = None
|
|
161
|
+
phase: Literal["new_moon", "full_moon"] | None = None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class SkyEventsResponse(ResponseModel):
|
|
165
|
+
date: str
|
|
166
|
+
timezone: str
|
|
167
|
+
events: list[SkyEvent]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class WesternSignRequest(RequestModel):
|
|
171
|
+
name: str = Field(default="Example User", max_length=200)
|
|
172
|
+
year: int = Field(ge=1, le=9999)
|
|
173
|
+
month: int = Field(ge=1, le=12)
|
|
174
|
+
day: int = Field(ge=1, le=31)
|
|
175
|
+
hour: int = Field(default=12, ge=0, le=23)
|
|
176
|
+
minute: int = Field(default=0, ge=0, le=59)
|
|
177
|
+
time_known: bool = True
|
|
178
|
+
city: str | None = Field(default=None, min_length=1, max_length=200)
|
|
179
|
+
lat: float | None = Field(default=None, ge=-90, le=90)
|
|
180
|
+
lng: float | None = Field(default=None, ge=-180, le=180)
|
|
181
|
+
tz_str: str = "AUTO"
|
|
182
|
+
house_system: Literal["placidus", "whole_sign", "equal", "regiomontanus", "koch", "porphyry"] = "placidus"
|
|
183
|
+
zodiac_type: Literal["tropical", "sidereal"] = "tropical"
|
|
184
|
+
sidereal_ayanamsa: Literal["lahiri", "raman", "kp", "fagan_bradley"] = "lahiri"
|
|
185
|
+
|
|
186
|
+
@model_validator(mode="after")
|
|
187
|
+
def require_location(self) -> WesternSignRequest:
|
|
188
|
+
if not self.city and (self.lat is None or self.lng is None):
|
|
189
|
+
raise ValueError("provide city or both lat and lng")
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class WesternSignResponse(ResponseModel):
|
|
194
|
+
point: str
|
|
195
|
+
sign: str
|
|
196
|
+
sign_id: Literal[
|
|
197
|
+
"aries",
|
|
198
|
+
"taurus",
|
|
199
|
+
"gemini",
|
|
200
|
+
"cancer",
|
|
201
|
+
"leo",
|
|
202
|
+
"virgo",
|
|
203
|
+
"libra",
|
|
204
|
+
"scorpio",
|
|
205
|
+
"sagittarius",
|
|
206
|
+
"capricorn",
|
|
207
|
+
"aquarius",
|
|
208
|
+
"pisces",
|
|
209
|
+
]
|
|
210
|
+
degree: float
|
|
211
|
+
abs_pos: float
|
|
212
|
+
house: int
|
|
213
|
+
interpretation: None = None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class ErrorResponse(ResponseModel):
|
|
217
|
+
detail: Any | None = None
|
|
218
|
+
error: str | None = None
|
|
219
|
+
message: str | None = None
|
|
220
|
+
code: str | None = None
|
|
221
|
+
trace_id: str | None = None
|
freeastroapi/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|