roxy-sdk 1.1.8__tar.gz → 1.2.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.
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/PKG-INFO +117 -68
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/README.md +116 -67
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/pyproject.toml +1 -1
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/pyproject.toml.orig +1 -1
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/src/roxy_sdk/AGENTS.md +125 -54
- roxy_sdk-1.2.0/src/roxy_sdk/__init__.py +7 -0
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/src/roxy_sdk/factory.py +244 -32
- roxy_sdk-1.2.0/src/roxy_sdk/version.py +1 -0
- roxy_sdk-1.1.8/src/roxy_sdk/__init__.py +0 -7
- roxy_sdk-1.1.8/src/roxy_sdk/version.py +0 -1
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/LICENSE +0 -0
- {roxy_sdk-1.1.8 → roxy_sdk-1.2.0}/src/roxy_sdk/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: roxy-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Python SDK for RoxyAPI. Astrology, tarot, numerology, and more.
|
|
5
5
|
Keywords: astrology,vedic,tarot,numerology,horoscope,kundli,birth-chart,natal-chart,iching,crystals,angel-numbers,dreams,biorhythm,spiritual,api,sdk,async,fastapi,ai-agent,mcp,llm
|
|
6
6
|
License-Expression: MIT
|
|
@@ -62,11 +62,13 @@ print(horoscope["overview"], horoscope["love"], horoscope["luckyNumber"])
|
|
|
62
62
|
|
|
63
63
|
Then expand into charts, compatibility, tarot, numerology, and more.
|
|
64
64
|
|
|
65
|
-
##
|
|
65
|
+
## Quick start
|
|
66
66
|
|
|
67
67
|
```python
|
|
68
68
|
from roxy_sdk import create_roxy
|
|
69
69
|
|
|
70
|
+
# Retries 429, 500, 502, 503 and 504 by default. Optional settings, shown with their defaults:
|
|
71
|
+
# create_roxy(key, timeout=30.0, max_retries=2, initial_delay=0.5, max_delay=30.0)
|
|
70
72
|
roxy = create_roxy("your-api-key")
|
|
71
73
|
|
|
72
74
|
# Step 1: geocode the birth city (required for any chart endpoint)
|
|
@@ -94,9 +96,7 @@ kundli = roxy.vedic_astrology.generate_birth_chart(
|
|
|
94
96
|
)
|
|
95
97
|
```
|
|
96
98
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
## Domain reference
|
|
99
|
+
## Domains
|
|
100
100
|
|
|
101
101
|
<!-- BEGIN:DOMAINS -->
|
|
102
102
|
| Domain | Property | Methods | What it covers |
|
|
@@ -176,7 +176,7 @@ Kundli generators, matrimonial matching, muhurta and panchang apps, and KP pract
|
|
|
176
176
|
```python
|
|
177
177
|
# Vedic kundli. The same birth read sidereally: `birth` reuses the location lookup above.
|
|
178
178
|
kundli = roxy.vedic_astrology.generate_birth_chart(**birth)
|
|
179
|
-
# kundli["meta"]["Moon"]["rashi"], kundli["meta"]["Moon"]["nakshatra"], kundli["houses"], kundli["combustion"]
|
|
179
|
+
# kundli["meta"]["Moon"]["rashi"] (a string), kundli["meta"]["Moon"]["nakshatra"]["name"] (nakshatra is an object with name, pada, lord), kundli["houses"], kundli["combustion"]
|
|
180
180
|
|
|
181
181
|
# Detailed panchang. Tithi, nakshatra, yoga, karana, rahu kaal and the muhurtas for a date and place.
|
|
182
182
|
panchang = roxy.vedic_astrology.get_detailed_panchang(date="2026-10-01", latitude=latitude, longitude=longitude, timezone=timezone)
|
|
@@ -447,7 +447,7 @@ sequence = roxy.angel_numbers.analyze_number_sequence(number="4242")
|
|
|
447
447
|
# sequence["digitRoot"], sequence["isRepeating"], sequence["knownMeaning"] (None when not curated), sequence["digitRootMeaning"]["title"]
|
|
448
448
|
```
|
|
449
449
|
|
|
450
|
-
## Built for AI agents (Claude Code,
|
|
450
|
+
## Built for AI agents (Cursor, Claude Code, Copilot, Codex, Gemini CLI)
|
|
451
451
|
|
|
452
452
|
<p align="center">
|
|
453
453
|
<img src="https://raw.githubusercontent.com/RoxyAPI/sdk-python/main/assets/agents.png" alt="Your coding agent already knows the API. Built for AI agents, Remote MCP, no local setup." width="100%">
|
|
@@ -462,23 +462,21 @@ claude mcp add-json --scope user roxy-astrology \
|
|
|
462
462
|
'{"type":"http","url":"https://roxyapi.com/mcp/astrology","headers":{"X-API-Key":"YOUR_KEY"}}'
|
|
463
463
|
```
|
|
464
464
|
|
|
465
|
-
##
|
|
465
|
+
## Authentication
|
|
466
466
|
|
|
467
|
-
|
|
467
|
+
Get your API key at [roxyapi.com/pricing](https://roxyapi.com/pricing). Instant delivery after checkout. Free test keys available on the [interactive docs](https://roxyapi.com/api-reference).
|
|
468
|
+
|
|
469
|
+
Store your API key in an environment variable for production:
|
|
468
470
|
|
|
469
471
|
```python
|
|
470
|
-
import
|
|
472
|
+
import os
|
|
471
473
|
from roxy_sdk import create_roxy
|
|
472
474
|
|
|
473
|
-
|
|
474
|
-
roxy = create_roxy("your-api-key")
|
|
475
|
-
horoscope = await roxy.astrology.get_daily_horoscope_async(sign="aries")
|
|
476
|
-
card = await roxy.tarot.get_daily_card_async()
|
|
477
|
-
print(horoscope, card)
|
|
478
|
-
|
|
479
|
-
asyncio.run(main())
|
|
475
|
+
roxy = create_roxy(os.environ["ROXY_API_KEY"])
|
|
480
476
|
```
|
|
481
477
|
|
|
478
|
+
Never expose your API key client-side. Call Roxy from server code only.
|
|
479
|
+
|
|
482
480
|
## Multi-language responses
|
|
483
481
|
|
|
484
482
|
<!-- BEGIN:LANGS -->
|
|
@@ -492,6 +490,95 @@ life_path = roxy.numerology.calculate_life_path(year=1990, month=1, day=15, lang
|
|
|
492
490
|
|
|
493
491
|
The two Chinese scripts (`zh-Hans`, `zh-Hant`) currently ship on Chinese astrology and feng shui; every other domain answers those codes in English per field.
|
|
494
492
|
|
|
493
|
+
## Error handling
|
|
494
|
+
|
|
495
|
+
All API errors raise `RoxyAPIError` with `error` (human-readable message), `code` (machine-readable, stable), `status_code`, `doc_url`, `retry_after` and `issues` attributes. `issues` lists every failing field of a `validation_error`, each with `path`, `message` and `code`, and is `None` for any other error. `ErrorCode` is a string enum of every code below, so `e.code == ErrorCode.RATE_LIMIT_PER_MINUTE` works:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
from roxy_sdk import create_roxy, ErrorCode, RoxyAPIError
|
|
499
|
+
|
|
500
|
+
roxy = create_roxy("your-api-key")
|
|
501
|
+
|
|
502
|
+
try:
|
|
503
|
+
result = roxy.astrology.get_daily_horoscope(sign="invalid")
|
|
504
|
+
except RoxyAPIError as e:
|
|
505
|
+
if e.code == ErrorCode.VALIDATION_ERROR:
|
|
506
|
+
for issue in e.issues or []:
|
|
507
|
+
print(issue["path"], issue["message"], issue["code"])
|
|
508
|
+
print(f"Code: {e.code}")
|
|
509
|
+
print(f"Error: {e.error}")
|
|
510
|
+
print(f"Status: {e.status_code}")
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
<!-- BEGIN:ERRORS -->
|
|
514
|
+
Every error returns `{ error, code, doc_url }`. Switch on `code`; `error` is readable text and may change. Each meaning below says whether and when to retry, and `Retry-After` carries the wait when the response sends it. Every code is documented at https://roxyapi.com/docs/errors.
|
|
515
|
+
|
|
516
|
+
| Status | Code | Meaning |
|
|
517
|
+
|--------|------|---------|
|
|
518
|
+
| 400 | `validation_error` | A field is missing or invalid. The body lists every failing field at once in issues, each with its path and the expected shape. |
|
|
519
|
+
| 400 | `bad_request` | The request was rejected as a whole, for example a body that is not valid JSON or a date range that ends before it starts. |
|
|
520
|
+
| 400 | `date_out_of_range` | A date lies outside the span the planetary positions are served for. The error names the supported dates. |
|
|
521
|
+
| 401 | `api_key_required` | No API key was sent. |
|
|
522
|
+
| 401 | `invalid_api_key` | The key sent is malformed or cannot be verified. |
|
|
523
|
+
| 401 | `api_key_revoked` | The key was valid once and has since been revoked from the account page. |
|
|
524
|
+
| 401 | `subscription_not_found` | The key verifies, but the subscription behind it no longer exists. |
|
|
525
|
+
| 401 | `subscription_inactive` | The subscription behind the key is cancelled, expired or suspended, or a trial is past its end date. |
|
|
526
|
+
| 401 | `publishable_key_not_allowed_on_mcp` | A publishable key was used on a Remote MCP server, which takes a secret key. |
|
|
527
|
+
| 401 | `invalid_client_ip` | A keyless request could not be attributed to a caller. |
|
|
528
|
+
| 401 | `unauthorized` | Authentication failed with no more specific reason to give. |
|
|
529
|
+
| 403 | `forbidden` | The key is valid, but this request is not permitted. |
|
|
530
|
+
| 403 | `origin_required` | A publishable key with an origin allowlist was used with no Origin header, as from a server or a native app. |
|
|
531
|
+
| 403 | `origin_not_allowed` | The calling site is not on the origin allowlist of this publishable key. |
|
|
532
|
+
| 404 | `not_found` | Nothing is served at this path, or the identifier in it does not exist. A path miss adds suggestions, the closest real endpoints. |
|
|
533
|
+
| 405 | `method_not_allowed` | The path exists for a different HTTP method. The body lists the accepted methods in allow, as does the Allow header. |
|
|
534
|
+
| 409 | `conflict` | The request cannot be applied in the current state. |
|
|
535
|
+
| 410 | `gone` | The path was removed permanently. |
|
|
536
|
+
| 413 | `payload_too_large` | The request body is larger than the API accepts. A body far over the cap is refused before it reaches the API and returns a bare 413 with no JSON body. |
|
|
537
|
+
| 415 | `unsupported_media_type` | A request with a body was sent without Content-Type: application/json. |
|
|
538
|
+
| 422 | `unprocessable_entity` | Every field is valid, but the combination cannot be computed. |
|
|
539
|
+
| 429 | `rate_limit_exceeded` | The monthly request allowance is used up. Retry-After gives the seconds to the reset. |
|
|
540
|
+
| 429 | `rate_limit_per_minute` | The per-minute limit of the plan was reached. Retry-After gives the seconds to the next minute, and the monthly allowance is not charged. |
|
|
541
|
+
| 429 | `rate_limited` | A rate limit was reached, reported without the plan detail. |
|
|
542
|
+
| 429 | `free_tier_exhausted` | A keyless request has used up its trial allowance. |
|
|
543
|
+
| 500 | `internal_error` | The request failed inside our service. Retry with exponential backoff. |
|
|
544
|
+
| 503 | `compute_saturated` | Heavy calculations are briefly at capacity. Send the same request again after Retry-After seconds. |
|
|
545
|
+
| Any | `error` | A fallback for a status with no more specific code. The status and the error field carry the signal. |
|
|
546
|
+
<!-- END:ERRORS -->
|
|
547
|
+
|
|
548
|
+
### Retries
|
|
549
|
+
|
|
550
|
+
The client retries twice by default: HTTP 429, 500, 502, 503 and 504, plus connection failures where the request was never sent. Pass `max_retries` to change the count, or `max_retries=0` to turn retries off. Waits use the `Retry-After` header when present, otherwise jittered exponential backoff from `initial_delay` (default 0.5 seconds) up to `max_delay` (default 30 seconds). A `Retry-After` longer than `max_delay`, such as the one on `rate_limit_exceeded`, is not waited out: the error is raised with `retry_after` set. A retry never sleeps past `timeout`. Read timeouts are never retried.
|
|
551
|
+
|
|
552
|
+
```python
|
|
553
|
+
from roxy_sdk import create_roxy, RoxyAPIError
|
|
554
|
+
|
|
555
|
+
roxy = create_roxy("your-api-key", max_retries=4)
|
|
556
|
+
|
|
557
|
+
try:
|
|
558
|
+
result = roxy.astrology.get_daily_horoscope(sign="aries")
|
|
559
|
+
except RoxyAPIError as e:
|
|
560
|
+
print(e.code) # "rate_limit_per_minute", "compute_saturated", "validation_error", ...
|
|
561
|
+
print(e.doc_url) # documentation page for that code
|
|
562
|
+
print(e.retry_after) # seconds the server asked you to wait, or None
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Async support
|
|
566
|
+
|
|
567
|
+
Every method has an `_async` suffix variant for use with asyncio:
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
import asyncio
|
|
571
|
+
from roxy_sdk import create_roxy
|
|
572
|
+
|
|
573
|
+
async def main():
|
|
574
|
+
roxy = create_roxy("your-api-key")
|
|
575
|
+
horoscope = await roxy.astrology.get_daily_horoscope_async(sign="aries")
|
|
576
|
+
card = await roxy.tarot.get_daily_card_async()
|
|
577
|
+
print(horoscope, card)
|
|
578
|
+
|
|
579
|
+
asyncio.run(main())
|
|
580
|
+
```
|
|
581
|
+
|
|
495
582
|
## Framework examples
|
|
496
583
|
|
|
497
584
|
The SDK is framework-agnostic. Works with Django, Flask, FastAPI, or any Python project.
|
|
@@ -536,51 +623,6 @@ def horoscope(request, sign):
|
|
|
536
623
|
return JsonResponse(roxy.astrology.get_daily_horoscope(sign=sign))
|
|
537
624
|
```
|
|
538
625
|
|
|
539
|
-
## Error handling
|
|
540
|
-
|
|
541
|
-
All API errors raise `RoxyAPIError` with `error` (human-readable message), `code` (machine-readable, stable), and `status_code` attributes:
|
|
542
|
-
|
|
543
|
-
```python
|
|
544
|
-
from roxy_sdk import create_roxy, RoxyAPIError
|
|
545
|
-
|
|
546
|
-
roxy = create_roxy("your-api-key")
|
|
547
|
-
|
|
548
|
-
try:
|
|
549
|
-
result = roxy.astrology.get_daily_horoscope(sign="invalid")
|
|
550
|
-
except RoxyAPIError as e:
|
|
551
|
-
print(f"Code: {e.code}")
|
|
552
|
-
print(f"Error: {e.error}")
|
|
553
|
-
print(f"Status: {e.status_code}")
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
| Status | Code | When |
|
|
557
|
-
|--------|------|------|
|
|
558
|
-
| 400 | `validation_error` | Missing or invalid parameters |
|
|
559
|
-
| 401 | `api_key_required` | No API key provided |
|
|
560
|
-
| 401 | `invalid_api_key` | Key format invalid or tampered |
|
|
561
|
-
| 401 | `subscription_not_found` | Key references non-existent subscription |
|
|
562
|
-
| 401 | `subscription_inactive` | Subscription cancelled, expired, or suspended |
|
|
563
|
-
| 401 | `api_key_revoked` | Key was deleted from the account |
|
|
564
|
-
| 404 | `not_found` | Resource not found |
|
|
565
|
-
| 4xx | `bad_request` and other status-derived codes | A client error the endpoint itself detected, such as a date window whose `endDate` precedes `startDate` |
|
|
566
|
-
| 429 | `rate_limit_exceeded` | Monthly quota reached |
|
|
567
|
-
| 500 | `internal_error` | Server error |
|
|
568
|
-
|
|
569
|
-
Switch on `code`, not `error`. Messages may be reworded; codes are stable.
|
|
570
|
-
|
|
571
|
-
## Authentication
|
|
572
|
-
|
|
573
|
-
Store your API key in an environment variable for production:
|
|
574
|
-
|
|
575
|
-
```python
|
|
576
|
-
import os
|
|
577
|
-
from roxy_sdk import create_roxy
|
|
578
|
-
|
|
579
|
-
roxy = create_roxy(os.environ["ROXY_API_KEY"])
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
Never expose your API key client-side. Call Roxy from server code only.
|
|
583
|
-
|
|
584
626
|
## Configuration
|
|
585
627
|
|
|
586
628
|
`create_roxy` accepts optional parameters for advanced usage:
|
|
@@ -589,7 +631,10 @@ Never expose your API key client-side. Call Roxy from server code only.
|
|
|
589
631
|
roxy = create_roxy(
|
|
590
632
|
api_key="your-api-key",
|
|
591
633
|
base_url="https://roxyapi.com/api/v2", # default
|
|
592
|
-
timeout=30.0, # request timeout in seconds
|
|
634
|
+
timeout=30.0, # request timeout in seconds, also the budget a retry never sleeps past
|
|
635
|
+
max_retries=2, # default; 0 turns retries off
|
|
636
|
+
initial_delay=0.5, # default backoff base in seconds
|
|
637
|
+
max_delay=30.0, # default longest wait in seconds
|
|
593
638
|
)
|
|
594
639
|
```
|
|
595
640
|
|
|
@@ -603,10 +648,14 @@ with create_roxy("your-api-key") as roxy:
|
|
|
603
648
|
|
|
604
649
|
## Links
|
|
605
650
|
|
|
606
|
-
- [
|
|
607
|
-
- [
|
|
651
|
+
- [Documentation](https://roxyapi.com/docs)
|
|
652
|
+
- [API Reference](https://roxyapi.com/api-reference)
|
|
608
653
|
- [Pricing](https://roxyapi.com/pricing)
|
|
609
|
-
- [MCP for AI
|
|
654
|
+
- [MCP setup for AI agents](https://roxyapi.com/docs/mcp)
|
|
610
655
|
- [Templates](https://roxyapi.com/templates)
|
|
611
|
-
- [
|
|
612
|
-
- [
|
|
656
|
+
- [Issues](https://github.com/RoxyAPI/sdk-python/issues)
|
|
657
|
+
- Other SDKs: [TypeScript](https://www.npmjs.com/package/@roxyapi/sdk), [PHP](https://packagist.org/packages/roxyapi/sdk), [.NET](https://www.nuget.org/packages/RoxyApi.Sdk), [Go](https://pkg.go.dev/github.com/RoxyAPI/sdk-go), [Rust](https://crates.io/crates/roxyapi)
|
|
658
|
+
|
|
659
|
+
## License
|
|
660
|
+
|
|
661
|
+
MIT
|
|
@@ -39,11 +39,13 @@ print(horoscope["overview"], horoscope["love"], horoscope["luckyNumber"])
|
|
|
39
39
|
|
|
40
40
|
Then expand into charts, compatibility, tarot, numerology, and more.
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## Quick start
|
|
43
43
|
|
|
44
44
|
```python
|
|
45
45
|
from roxy_sdk import create_roxy
|
|
46
46
|
|
|
47
|
+
# Retries 429, 500, 502, 503 and 504 by default. Optional settings, shown with their defaults:
|
|
48
|
+
# create_roxy(key, timeout=30.0, max_retries=2, initial_delay=0.5, max_delay=30.0)
|
|
47
49
|
roxy = create_roxy("your-api-key")
|
|
48
50
|
|
|
49
51
|
# Step 1: geocode the birth city (required for any chart endpoint)
|
|
@@ -71,9 +73,7 @@ kundli = roxy.vedic_astrology.generate_birth_chart(
|
|
|
71
73
|
)
|
|
72
74
|
```
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## Domain reference
|
|
76
|
+
## Domains
|
|
77
77
|
|
|
78
78
|
<!-- BEGIN:DOMAINS -->
|
|
79
79
|
| Domain | Property | Methods | What it covers |
|
|
@@ -153,7 +153,7 @@ Kundli generators, matrimonial matching, muhurta and panchang apps, and KP pract
|
|
|
153
153
|
```python
|
|
154
154
|
# Vedic kundli. The same birth read sidereally: `birth` reuses the location lookup above.
|
|
155
155
|
kundli = roxy.vedic_astrology.generate_birth_chart(**birth)
|
|
156
|
-
# kundli["meta"]["Moon"]["rashi"], kundli["meta"]["Moon"]["nakshatra"], kundli["houses"], kundli["combustion"]
|
|
156
|
+
# kundli["meta"]["Moon"]["rashi"] (a string), kundli["meta"]["Moon"]["nakshatra"]["name"] (nakshatra is an object with name, pada, lord), kundli["houses"], kundli["combustion"]
|
|
157
157
|
|
|
158
158
|
# Detailed panchang. Tithi, nakshatra, yoga, karana, rahu kaal and the muhurtas for a date and place.
|
|
159
159
|
panchang = roxy.vedic_astrology.get_detailed_panchang(date="2026-10-01", latitude=latitude, longitude=longitude, timezone=timezone)
|
|
@@ -424,7 +424,7 @@ sequence = roxy.angel_numbers.analyze_number_sequence(number="4242")
|
|
|
424
424
|
# sequence["digitRoot"], sequence["isRepeating"], sequence["knownMeaning"] (None when not curated), sequence["digitRootMeaning"]["title"]
|
|
425
425
|
```
|
|
426
426
|
|
|
427
|
-
## Built for AI agents (Claude Code,
|
|
427
|
+
## Built for AI agents (Cursor, Claude Code, Copilot, Codex, Gemini CLI)
|
|
428
428
|
|
|
429
429
|
<p align="center">
|
|
430
430
|
<img src="https://raw.githubusercontent.com/RoxyAPI/sdk-python/main/assets/agents.png" alt="Your coding agent already knows the API. Built for AI agents, Remote MCP, no local setup." width="100%">
|
|
@@ -439,23 +439,21 @@ claude mcp add-json --scope user roxy-astrology \
|
|
|
439
439
|
'{"type":"http","url":"https://roxyapi.com/mcp/astrology","headers":{"X-API-Key":"YOUR_KEY"}}'
|
|
440
440
|
```
|
|
441
441
|
|
|
442
|
-
##
|
|
442
|
+
## Authentication
|
|
443
443
|
|
|
444
|
-
|
|
444
|
+
Get your API key at [roxyapi.com/pricing](https://roxyapi.com/pricing). Instant delivery after checkout. Free test keys available on the [interactive docs](https://roxyapi.com/api-reference).
|
|
445
|
+
|
|
446
|
+
Store your API key in an environment variable for production:
|
|
445
447
|
|
|
446
448
|
```python
|
|
447
|
-
import
|
|
449
|
+
import os
|
|
448
450
|
from roxy_sdk import create_roxy
|
|
449
451
|
|
|
450
|
-
|
|
451
|
-
roxy = create_roxy("your-api-key")
|
|
452
|
-
horoscope = await roxy.astrology.get_daily_horoscope_async(sign="aries")
|
|
453
|
-
card = await roxy.tarot.get_daily_card_async()
|
|
454
|
-
print(horoscope, card)
|
|
455
|
-
|
|
456
|
-
asyncio.run(main())
|
|
452
|
+
roxy = create_roxy(os.environ["ROXY_API_KEY"])
|
|
457
453
|
```
|
|
458
454
|
|
|
455
|
+
Never expose your API key client-side. Call Roxy from server code only.
|
|
456
|
+
|
|
459
457
|
## Multi-language responses
|
|
460
458
|
|
|
461
459
|
<!-- BEGIN:LANGS -->
|
|
@@ -469,6 +467,95 @@ life_path = roxy.numerology.calculate_life_path(year=1990, month=1, day=15, lang
|
|
|
469
467
|
|
|
470
468
|
The two Chinese scripts (`zh-Hans`, `zh-Hant`) currently ship on Chinese astrology and feng shui; every other domain answers those codes in English per field.
|
|
471
469
|
|
|
470
|
+
## Error handling
|
|
471
|
+
|
|
472
|
+
All API errors raise `RoxyAPIError` with `error` (human-readable message), `code` (machine-readable, stable), `status_code`, `doc_url`, `retry_after` and `issues` attributes. `issues` lists every failing field of a `validation_error`, each with `path`, `message` and `code`, and is `None` for any other error. `ErrorCode` is a string enum of every code below, so `e.code == ErrorCode.RATE_LIMIT_PER_MINUTE` works:
|
|
473
|
+
|
|
474
|
+
```python
|
|
475
|
+
from roxy_sdk import create_roxy, ErrorCode, RoxyAPIError
|
|
476
|
+
|
|
477
|
+
roxy = create_roxy("your-api-key")
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
result = roxy.astrology.get_daily_horoscope(sign="invalid")
|
|
481
|
+
except RoxyAPIError as e:
|
|
482
|
+
if e.code == ErrorCode.VALIDATION_ERROR:
|
|
483
|
+
for issue in e.issues or []:
|
|
484
|
+
print(issue["path"], issue["message"], issue["code"])
|
|
485
|
+
print(f"Code: {e.code}")
|
|
486
|
+
print(f"Error: {e.error}")
|
|
487
|
+
print(f"Status: {e.status_code}")
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
<!-- BEGIN:ERRORS -->
|
|
491
|
+
Every error returns `{ error, code, doc_url }`. Switch on `code`; `error` is readable text and may change. Each meaning below says whether and when to retry, and `Retry-After` carries the wait when the response sends it. Every code is documented at https://roxyapi.com/docs/errors.
|
|
492
|
+
|
|
493
|
+
| Status | Code | Meaning |
|
|
494
|
+
|--------|------|---------|
|
|
495
|
+
| 400 | `validation_error` | A field is missing or invalid. The body lists every failing field at once in issues, each with its path and the expected shape. |
|
|
496
|
+
| 400 | `bad_request` | The request was rejected as a whole, for example a body that is not valid JSON or a date range that ends before it starts. |
|
|
497
|
+
| 400 | `date_out_of_range` | A date lies outside the span the planetary positions are served for. The error names the supported dates. |
|
|
498
|
+
| 401 | `api_key_required` | No API key was sent. |
|
|
499
|
+
| 401 | `invalid_api_key` | The key sent is malformed or cannot be verified. |
|
|
500
|
+
| 401 | `api_key_revoked` | The key was valid once and has since been revoked from the account page. |
|
|
501
|
+
| 401 | `subscription_not_found` | The key verifies, but the subscription behind it no longer exists. |
|
|
502
|
+
| 401 | `subscription_inactive` | The subscription behind the key is cancelled, expired or suspended, or a trial is past its end date. |
|
|
503
|
+
| 401 | `publishable_key_not_allowed_on_mcp` | A publishable key was used on a Remote MCP server, which takes a secret key. |
|
|
504
|
+
| 401 | `invalid_client_ip` | A keyless request could not be attributed to a caller. |
|
|
505
|
+
| 401 | `unauthorized` | Authentication failed with no more specific reason to give. |
|
|
506
|
+
| 403 | `forbidden` | The key is valid, but this request is not permitted. |
|
|
507
|
+
| 403 | `origin_required` | A publishable key with an origin allowlist was used with no Origin header, as from a server or a native app. |
|
|
508
|
+
| 403 | `origin_not_allowed` | The calling site is not on the origin allowlist of this publishable key. |
|
|
509
|
+
| 404 | `not_found` | Nothing is served at this path, or the identifier in it does not exist. A path miss adds suggestions, the closest real endpoints. |
|
|
510
|
+
| 405 | `method_not_allowed` | The path exists for a different HTTP method. The body lists the accepted methods in allow, as does the Allow header. |
|
|
511
|
+
| 409 | `conflict` | The request cannot be applied in the current state. |
|
|
512
|
+
| 410 | `gone` | The path was removed permanently. |
|
|
513
|
+
| 413 | `payload_too_large` | The request body is larger than the API accepts. A body far over the cap is refused before it reaches the API and returns a bare 413 with no JSON body. |
|
|
514
|
+
| 415 | `unsupported_media_type` | A request with a body was sent without Content-Type: application/json. |
|
|
515
|
+
| 422 | `unprocessable_entity` | Every field is valid, but the combination cannot be computed. |
|
|
516
|
+
| 429 | `rate_limit_exceeded` | The monthly request allowance is used up. Retry-After gives the seconds to the reset. |
|
|
517
|
+
| 429 | `rate_limit_per_minute` | The per-minute limit of the plan was reached. Retry-After gives the seconds to the next minute, and the monthly allowance is not charged. |
|
|
518
|
+
| 429 | `rate_limited` | A rate limit was reached, reported without the plan detail. |
|
|
519
|
+
| 429 | `free_tier_exhausted` | A keyless request has used up its trial allowance. |
|
|
520
|
+
| 500 | `internal_error` | The request failed inside our service. Retry with exponential backoff. |
|
|
521
|
+
| 503 | `compute_saturated` | Heavy calculations are briefly at capacity. Send the same request again after Retry-After seconds. |
|
|
522
|
+
| Any | `error` | A fallback for a status with no more specific code. The status and the error field carry the signal. |
|
|
523
|
+
<!-- END:ERRORS -->
|
|
524
|
+
|
|
525
|
+
### Retries
|
|
526
|
+
|
|
527
|
+
The client retries twice by default: HTTP 429, 500, 502, 503 and 504, plus connection failures where the request was never sent. Pass `max_retries` to change the count, or `max_retries=0` to turn retries off. Waits use the `Retry-After` header when present, otherwise jittered exponential backoff from `initial_delay` (default 0.5 seconds) up to `max_delay` (default 30 seconds). A `Retry-After` longer than `max_delay`, such as the one on `rate_limit_exceeded`, is not waited out: the error is raised with `retry_after` set. A retry never sleeps past `timeout`. Read timeouts are never retried.
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
from roxy_sdk import create_roxy, RoxyAPIError
|
|
531
|
+
|
|
532
|
+
roxy = create_roxy("your-api-key", max_retries=4)
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
result = roxy.astrology.get_daily_horoscope(sign="aries")
|
|
536
|
+
except RoxyAPIError as e:
|
|
537
|
+
print(e.code) # "rate_limit_per_minute", "compute_saturated", "validation_error", ...
|
|
538
|
+
print(e.doc_url) # documentation page for that code
|
|
539
|
+
print(e.retry_after) # seconds the server asked you to wait, or None
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Async support
|
|
543
|
+
|
|
544
|
+
Every method has an `_async` suffix variant for use with asyncio:
|
|
545
|
+
|
|
546
|
+
```python
|
|
547
|
+
import asyncio
|
|
548
|
+
from roxy_sdk import create_roxy
|
|
549
|
+
|
|
550
|
+
async def main():
|
|
551
|
+
roxy = create_roxy("your-api-key")
|
|
552
|
+
horoscope = await roxy.astrology.get_daily_horoscope_async(sign="aries")
|
|
553
|
+
card = await roxy.tarot.get_daily_card_async()
|
|
554
|
+
print(horoscope, card)
|
|
555
|
+
|
|
556
|
+
asyncio.run(main())
|
|
557
|
+
```
|
|
558
|
+
|
|
472
559
|
## Framework examples
|
|
473
560
|
|
|
474
561
|
The SDK is framework-agnostic. Works with Django, Flask, FastAPI, or any Python project.
|
|
@@ -513,51 +600,6 @@ def horoscope(request, sign):
|
|
|
513
600
|
return JsonResponse(roxy.astrology.get_daily_horoscope(sign=sign))
|
|
514
601
|
```
|
|
515
602
|
|
|
516
|
-
## Error handling
|
|
517
|
-
|
|
518
|
-
All API errors raise `RoxyAPIError` with `error` (human-readable message), `code` (machine-readable, stable), and `status_code` attributes:
|
|
519
|
-
|
|
520
|
-
```python
|
|
521
|
-
from roxy_sdk import create_roxy, RoxyAPIError
|
|
522
|
-
|
|
523
|
-
roxy = create_roxy("your-api-key")
|
|
524
|
-
|
|
525
|
-
try:
|
|
526
|
-
result = roxy.astrology.get_daily_horoscope(sign="invalid")
|
|
527
|
-
except RoxyAPIError as e:
|
|
528
|
-
print(f"Code: {e.code}")
|
|
529
|
-
print(f"Error: {e.error}")
|
|
530
|
-
print(f"Status: {e.status_code}")
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
| Status | Code | When |
|
|
534
|
-
|--------|------|------|
|
|
535
|
-
| 400 | `validation_error` | Missing or invalid parameters |
|
|
536
|
-
| 401 | `api_key_required` | No API key provided |
|
|
537
|
-
| 401 | `invalid_api_key` | Key format invalid or tampered |
|
|
538
|
-
| 401 | `subscription_not_found` | Key references non-existent subscription |
|
|
539
|
-
| 401 | `subscription_inactive` | Subscription cancelled, expired, or suspended |
|
|
540
|
-
| 401 | `api_key_revoked` | Key was deleted from the account |
|
|
541
|
-
| 404 | `not_found` | Resource not found |
|
|
542
|
-
| 4xx | `bad_request` and other status-derived codes | A client error the endpoint itself detected, such as a date window whose `endDate` precedes `startDate` |
|
|
543
|
-
| 429 | `rate_limit_exceeded` | Monthly quota reached |
|
|
544
|
-
| 500 | `internal_error` | Server error |
|
|
545
|
-
|
|
546
|
-
Switch on `code`, not `error`. Messages may be reworded; codes are stable.
|
|
547
|
-
|
|
548
|
-
## Authentication
|
|
549
|
-
|
|
550
|
-
Store your API key in an environment variable for production:
|
|
551
|
-
|
|
552
|
-
```python
|
|
553
|
-
import os
|
|
554
|
-
from roxy_sdk import create_roxy
|
|
555
|
-
|
|
556
|
-
roxy = create_roxy(os.environ["ROXY_API_KEY"])
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
Never expose your API key client-side. Call Roxy from server code only.
|
|
560
|
-
|
|
561
603
|
## Configuration
|
|
562
604
|
|
|
563
605
|
`create_roxy` accepts optional parameters for advanced usage:
|
|
@@ -566,7 +608,10 @@ Never expose your API key client-side. Call Roxy from server code only.
|
|
|
566
608
|
roxy = create_roxy(
|
|
567
609
|
api_key="your-api-key",
|
|
568
610
|
base_url="https://roxyapi.com/api/v2", # default
|
|
569
|
-
timeout=30.0, # request timeout in seconds
|
|
611
|
+
timeout=30.0, # request timeout in seconds, also the budget a retry never sleeps past
|
|
612
|
+
max_retries=2, # default; 0 turns retries off
|
|
613
|
+
initial_delay=0.5, # default backoff base in seconds
|
|
614
|
+
max_delay=30.0, # default longest wait in seconds
|
|
570
615
|
)
|
|
571
616
|
```
|
|
572
617
|
|
|
@@ -580,10 +625,14 @@ with create_roxy("your-api-key") as roxy:
|
|
|
580
625
|
|
|
581
626
|
## Links
|
|
582
627
|
|
|
583
|
-
- [
|
|
584
|
-
- [
|
|
628
|
+
- [Documentation](https://roxyapi.com/docs)
|
|
629
|
+
- [API Reference](https://roxyapi.com/api-reference)
|
|
585
630
|
- [Pricing](https://roxyapi.com/pricing)
|
|
586
|
-
- [MCP for AI
|
|
631
|
+
- [MCP setup for AI agents](https://roxyapi.com/docs/mcp)
|
|
587
632
|
- [Templates](https://roxyapi.com/templates)
|
|
588
|
-
- [
|
|
589
|
-
- [
|
|
633
|
+
- [Issues](https://github.com/RoxyAPI/sdk-python/issues)
|
|
634
|
+
- Other SDKs: [TypeScript](https://www.npmjs.com/package/@roxyapi/sdk), [PHP](https://packagist.org/packages/roxyapi/sdk), [.NET](https://www.nuget.org/packages/RoxyApi.Sdk), [Go](https://pkg.go.dev/github.com/RoxyAPI/sdk-go), [Rust](https://crates.io/crates/roxyapi)
|
|
635
|
+
|
|
636
|
+
## License
|
|
637
|
+
|
|
638
|
+
MIT
|
|
@@ -11,22 +11,13 @@ pip install roxy-sdk
|
|
|
11
11
|
```python
|
|
12
12
|
from roxy_sdk import create_roxy
|
|
13
13
|
|
|
14
|
+
# Retries 429, 500, 502, 503 and 504 by default. Optional settings, shown with their defaults:
|
|
15
|
+
# create_roxy(key, timeout=30.0, max_retries=2, initial_delay=0.5, max_delay=30.0)
|
|
14
16
|
roxy = create_roxy("your-api-key")
|
|
15
17
|
```
|
|
16
18
|
|
|
17
19
|
`create_roxy` sets the base URL (`https://roxyapi.com/api/v2`) and auth header automatically. Returns a `Roxy` instance with namespaced domain properties.
|
|
18
20
|
|
|
19
|
-
## Quality guidelines for agents
|
|
20
|
-
|
|
21
|
-
- Every method returns `dict[str, Any]`. Access fields with `result["key"]["subkey"]`, never `result.key.subkey` (Python will raise `AttributeError: 'dict' object has no attribute ...`).
|
|
22
|
-
- Method names are snake_case versions of the OpenAPI operationId (e.g. `castThreeCard` -> `cast_three_card`, `analyzeNumberSequence` -> `analyze_number_sequence`). Every sync method has an `_async` variant; use `await` with the async variants.
|
|
23
|
-
- Param names are snake_case versions of the spec param names (`fullName` -> `full_name`, `birthDate` -> `birth_date`, `houseSystem` -> `house_system`). All params are keyword-only.
|
|
24
|
-
- When in doubt about a method or kwarg, check `roxy_sdk.factory` directly or run `python -c "from roxy_sdk import Roxy; help(Roxy)"` - the generated signatures are the contract.
|
|
25
|
-
- Response field names match the response schema of the OpenAPI spec exactly. Never invent or pluralize. If a field is not in the spec, it is not in the response.
|
|
26
|
-
- To look up any operation or field beyond this guide, query the combined OpenAPI spec at `https://roxyapi.com/api/v2/openapi.json` with the jq recipe in `https://roxyapi.com/AGENTS.md`, or search the keyless Docs MCP server at `https://roxyapi.com/mcp/docs` (one tool, `search_docs`).
|
|
27
|
-
- Strings everywhere: `date` is `"YYYY-MM-DD"`, `time` is `"HH:MM:SS"`, `timezone` is an IANA name (`"Asia/Kolkata"`, `"America/New_York"`) - the server resolves DST for the chart date. `number` (angel) is `"1111"`, `month` (birthstone) is `"1"`, `number` (hexagram) is `"1"`. Numeric kwargs are only `latitude`, `longitude`, `year`, `month` (numerology), `day`, `count` (tarot draw).
|
|
28
|
-
- Inside `person1` / `person2` dicts any value type works because the kwarg is typed `dict[str, Any]`; only the top-level `timezone` kwarg needs string form.
|
|
29
|
-
|
|
30
21
|
## Critical rule: geocode before any chart endpoint
|
|
31
22
|
|
|
32
23
|
Every chart, horoscope, panchang, dasha, dosha, navamsa, KP, synastry, compatibility, and natal endpoint needs `latitude`, `longitude`, and (for Western) `timezone`. **Never ask the user for coordinates.** Always call `roxy.location.search_cities` first.
|
|
@@ -36,9 +27,9 @@ result = roxy.location.search_cities(q="New York")
|
|
|
36
27
|
city = result["cities"][0]
|
|
37
28
|
lat, lng, tz = city["latitude"], city["longitude"], city["timezone"]
|
|
38
29
|
# `timezone` is the IANA string ("America/New_York"). Pass it directly to any chart endpoint.
|
|
39
|
-
# The server resolves it to the DST-correct decimal offset using the date of the
|
|
30
|
+
# The server resolves it to the DST-correct decimal offset using the date of the chart itself,
|
|
40
31
|
# so a January 1990 New York chart gets EST (-5) even when you looked the city up in July.
|
|
41
|
-
#
|
|
32
|
+
# city["utcOffset"] is the offset in force today, not on the birth date, so pass the IANA `timezone` string.
|
|
42
33
|
```
|
|
43
34
|
|
|
44
35
|
`q` accepts bare city (`"Paris"`), city + country (`"Berlin Germany"`), or comma-qualified (`"Springfield, Illinois"`). Use the qualified form to disambiguate same-named cities.
|
|
@@ -72,6 +63,20 @@ Type `roxy.` to see all available namespaces. Type `roxy.{domain}.` in an IDE wi
|
|
|
72
63
|
| [`roxy.languages`](https://roxyapi.com/api-reference/languages) | 2 | List the response languages accepted by the `lang` query parameter on every i18n-aware endpoint |
|
|
73
64
|
<!-- END:DOMAINS -->
|
|
74
65
|
|
|
66
|
+
## Quality guidelines for agents
|
|
67
|
+
|
|
68
|
+
The rules to follow when writing any call with this SDK. Get these right and the generated signatures do the rest.
|
|
69
|
+
|
|
70
|
+
- **Responses are dicts.** Every method returns `dict[str, Any]`. Access fields with `result["key"]["subkey"]`, never `result.key.subkey` (Python will raise `AttributeError: 'dict' object has no attribute ...`).
|
|
71
|
+
- **Method names are snake_case versions of the OpenAPI operationId** (e.g. `castThreeCard` -> `cast_three_card`, `analyzeNumberSequence` -> `analyze_number_sequence`). Every sync method has an `_async` variant; use `await` with the async variants.
|
|
72
|
+
- **Param names are snake_case versions of the spec param names** (`fullName` -> `full_name`, `birthDate` -> `birth_date`, `houseSystem` -> `house_system`). All params are keyword-only.
|
|
73
|
+
- **When in doubt about a method or kwarg, read the generated signatures.** Check `roxy_sdk.factory` directly or run `python -c "from roxy_sdk import Roxy; help(Roxy)"` - the generated signatures are the contract.
|
|
74
|
+
- **Response field names come from the response schema of the spec.** They match it exactly. Never invent or pluralize. If a field is not in the spec, it is not in the response.
|
|
75
|
+
- **Look up any operation or field beyond this guide.** Query the combined OpenAPI spec at `https://roxyapi.com/api/v2/openapi.json` with the jq recipe in `https://roxyapi.com/AGENTS.md`, or search the keyless Docs MCP server at `https://roxyapi.com/mcp/docs` (one tool, `search_docs`).
|
|
76
|
+
- **Strings everywhere.** `date` is `"YYYY-MM-DD"`, `time` is `"HH:MM:SS"`, `timezone` is an IANA name (`"Asia/Kolkata"`, `"America/New_York"`) - the server resolves DST for the chart date. `number` (angel) is `"1111"`, `month` (birthstone) is `"1"`, `number` (hexagram) is `"1"`. Numeric kwargs are only `latitude`, `longitude`, `year`, `month` (numerology), `day`, `count` (tarot draw).
|
|
77
|
+
- **Nested dicts accept any value type.** Inside `person1` / `person2` dicts any value type works because the kwarg is typed `dict[str, Any]`; only the top-level `timezone` kwarg needs string form.
|
|
78
|
+
- **Do not hand-roll requests.** No raw HTTP calls. The SDK injects auth, the base URL and the error type; it retries 429, 500, 502, 503 and 504 up to twice by default (see Retries; `create_roxy(key, max_retries=0)` turns that off).
|
|
79
|
+
|
|
75
80
|
## Critical patterns
|
|
76
81
|
|
|
77
82
|
### Two-step pattern for coordinate-dependent endpoints
|
|
@@ -81,8 +86,10 @@ result = roxy.location.search_cities(q="London")
|
|
|
81
86
|
city = result["cities"][0]
|
|
82
87
|
|
|
83
88
|
chart = roxy.astrology.generate_natal_chart(
|
|
84
|
-
date="1990-01-15",
|
|
85
|
-
|
|
89
|
+
date="1990-01-15",
|
|
90
|
+
time="14:30:00",
|
|
91
|
+
latitude=city["latitude"],
|
|
92
|
+
longitude=city["longitude"],
|
|
86
93
|
timezone=city["timezone"], # IANA string, server resolves DST for the date
|
|
87
94
|
)
|
|
88
95
|
```
|
|
@@ -112,19 +119,27 @@ Most valuable endpoints are POST:
|
|
|
112
119
|
|
|
113
120
|
```python
|
|
114
121
|
natal = roxy.astrology.generate_natal_chart(
|
|
115
|
-
date="1990-01-15",
|
|
116
|
-
|
|
122
|
+
date="1990-01-15",
|
|
123
|
+
time="14:30:00",
|
|
124
|
+
latitude=40.7128,
|
|
125
|
+
longitude=-74.006,
|
|
126
|
+
timezone="America/New_York",
|
|
117
127
|
)
|
|
118
128
|
|
|
119
129
|
kundli = roxy.vedic_astrology.generate_birth_chart(
|
|
120
|
-
date="1990-01-15",
|
|
121
|
-
|
|
130
|
+
date="1990-01-15",
|
|
131
|
+
time="14:30:00",
|
|
132
|
+
latitude=28.6139,
|
|
133
|
+
longitude=77.209,
|
|
122
134
|
)
|
|
123
135
|
|
|
124
136
|
celtic = roxy.tarot.cast_celtic_cross(question="What should I focus on?")
|
|
125
137
|
|
|
126
138
|
numerology = roxy.numerology.generate_numerology_chart(
|
|
127
|
-
full_name="John Doe",
|
|
139
|
+
full_name="John Doe",
|
|
140
|
+
year=1990,
|
|
141
|
+
month=1,
|
|
142
|
+
day=15,
|
|
128
143
|
)
|
|
129
144
|
```
|
|
130
145
|
|
|
@@ -141,34 +156,6 @@ life_path = roxy.numerology.calculate_life_path(year=1990, month=1, day=15, lang
|
|
|
141
156
|
|
|
142
157
|
The two Chinese scripts (`zh-Hans`, `zh-Hant`) currently ship on Chinese astrology and feng shui; every other domain answers those codes in English per field. To list supported codes at runtime, call `roxy.languages.list_languages()`.
|
|
143
158
|
|
|
144
|
-
### Error handling
|
|
145
|
-
|
|
146
|
-
Errors raise `RoxyAPIError` with `error` (message), `code` (machine-readable), and `status_code`:
|
|
147
|
-
|
|
148
|
-
```python
|
|
149
|
-
from roxy_sdk import create_roxy, RoxyAPIError
|
|
150
|
-
|
|
151
|
-
try:
|
|
152
|
-
result = roxy.astrology.get_daily_horoscope(sign="invalid")
|
|
153
|
-
except RoxyAPIError as e:
|
|
154
|
-
print(e.code) # "validation_error"
|
|
155
|
-
print(e.error) # "Invalid sign"
|
|
156
|
-
print(e.status_code) # 400
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
| Status | Code | When |
|
|
160
|
-
|--------|------|------|
|
|
161
|
-
| 400 | `validation_error` | Missing or invalid parameters |
|
|
162
|
-
| 401 | `api_key_required` | No API key provided |
|
|
163
|
-
| 401 | `invalid_api_key` | Key format invalid or tampered |
|
|
164
|
-
| 401 | `subscription_not_found` | Key references non-existent subscription |
|
|
165
|
-
| 401 | `subscription_inactive` | Subscription cancelled, expired, or suspended |
|
|
166
|
-
| 401 | `api_key_revoked` | Key was deleted from the account |
|
|
167
|
-
| 404 | `not_found` | Resource not found |
|
|
168
|
-
| 4xx | `bad_request` and other status-derived codes | A client error the endpoint itself detected, such as a date window whose `endDate` precedes `startDate` |
|
|
169
|
-
| 429 | `rate_limit_exceeded` | Monthly quota reached |
|
|
170
|
-
| 500 | `internal_error` | Server error |
|
|
171
|
-
|
|
172
159
|
## Common tasks
|
|
173
160
|
|
|
174
161
|
In the catalog order (Western astrology, Vedic astrology, forecast, Human Design, Chinese astrology, feng shui, Mesoamerican astrology, Vastu, numerology, Kabbalah, tarot, biorhythm, Ayurveda, I Ching, crystals, dreams, angel numbers, location, usage, languages). `birth` is `{"date": ..., "time": ..., "latitude": ..., "longitude": ..., "timezone": ...}` from the two-step pattern above.
|
|
@@ -207,7 +194,7 @@ In the catalog order (Western astrology, Vedic astrology, forecast, Human Design
|
|
|
207
194
|
| Vastu room compliance | `roxy.vastu.calculate_room_compliance(plot=plot, facing=facing, rooms=rooms)` |
|
|
208
195
|
| Life path number | `roxy.numerology.calculate_life_path(year=year, month=month, day=day)` |
|
|
209
196
|
| Full numerology chart | `roxy.numerology.generate_numerology_chart(full_name=full_name, year=year, month=month, day=day)` |
|
|
210
|
-
| Personal year | `roxy.numerology.calculate_personal_year(month=month, day=day)` |
|
|
197
|
+
| Personal year | `roxy.numerology.calculate_personal_year(month=month, day=day)` (`year` is optional, 100 to 2100, and defaults to the current UTC year) |
|
|
211
198
|
| Gematria | `roxy.kabbalah.calculate_gematria(text=text)` |
|
|
212
199
|
| Kabbalah birth profile | `roxy.kabbalah.generate_birth_profile(date=date, time=time, timezone=timezone)` |
|
|
213
200
|
| Daily tarot card | `roxy.tarot.get_daily_card(seed=seed)` |
|
|
@@ -242,7 +229,7 @@ These are the fields AI agents most often get wrong. Copy the format column exac
|
|
|
242
229
|
| `time` | 24-hour string | `"14:30:00"`, `"09:00:00"` | `"2:30 PM"`, `"14:30"` (no seconds), `"9:0:0"` (no leading zeros) |
|
|
243
230
|
| `latitude` | Decimal degrees (float) | `51.5074` (London), `-33.8688` (Sydney), `40.7128` (NYC) | `"28°36'N"`, `"28 36 50"`, strings |
|
|
244
231
|
| `longitude` | Decimal degrees (float) | `-0.1278` (London), `-74.006` (NYC), `139.6917` (Tokyo) | Same as latitude - no DMS strings |
|
|
245
|
-
| `sign` (horoscope kwarg) | Lowercase zodiac name | `"aries"`, `"taurus"`, `"gemini"`, ... `"pisces"` | `"
|
|
232
|
+
| `sign` (horoscope kwarg) | Lowercase zodiac name | `"aries"`, `"taurus"`, `"gemini"`, ... `"pisces"` | `"♈"`, `"1"` (the server matches case-insensitively, so `"Aries"` and `"ARIES"` work, but lowercase is canonical) |
|
|
246
233
|
| `full_name` (numerology) | Birth-certificate name | `"John William Smith"`, `"Priya Rajesh Sharma"` | Nickname, married name, partial name - affects all letter-based calcs |
|
|
247
234
|
| `seed` | Any string (deterministic) | `"user-42"`, `"session-abc-123"`, email hash | Numbers, objects - must be string |
|
|
248
235
|
| `number` (angel numbers) | String | `"1111"`, `"777"`, `"1234"` | `1111` (int) fails path validation |
|
|
@@ -283,13 +270,82 @@ LLMs hallucinate confidently in this category. These are the specific traps you
|
|
|
283
270
|
|
|
284
271
|
- **Ayanamsa is server-side in Vedic.** LLMs default to tropical / Western math. Vedic endpoints apply sidereal Lahiri ayanamsa server-side. KP endpoints accept `ayanamsa` of `kp-newcomb` (default), `kp-old`, `lahiri`, or `custom`. Do not try to "correct" server output by subtracting ayanamsa in client code.
|
|
285
272
|
- **Tithi count is 30, not 2.** 15 Shukla (waxing) plus 15 Krishna (waning). Older LLM training data conflates Purnima and Amavasya as single tithis. Our panchang response carries a `paksha` field (`"Shukla"` or `"Krishna"`) plus a tithi number, so there are 30 distinct tithis in a lunar month.
|
|
286
|
-
- **Rahu and Ketu are shadow points, not planets.** They do not appear in a real ephemeris. Endpoints accept `node_type` of `"mean"` (
|
|
273
|
+
- **Rahu and Ketu are shadow points, not planets.** They do not appear in a real ephemeris. Endpoints accept `node_type` of `"mean"` (smooth mean node, traditional Vedic default) or `"true"` (osculating node with perturbation corrections) to select which calculation to use.
|
|
287
274
|
- **Nakshatra count is 27.** Abhijit is sometimes treated as a 28th in some schools, but this API uses the standard 27. `roxy.vedic_astrology.list_nakshatras()` returns a list of length 27.
|
|
288
275
|
- **Retrograde is per-planet, not global.** Natal chart planets and Vedic `meta` include `isRetrograde: bool` per planet. KP planet lists use `retrograde`. Never generate "Mercury retrograde globally" UI copy, check the specific planet in the response.
|
|
289
276
|
- **Tarot reversals are a product choice.** `allow_reversals=False` on a tarot draw means no reversed cards in that draw, period. It is not cosmically meaningful, it is a config flag.
|
|
290
277
|
- **Angel number lookup works for any positive integer.** Digit-root fallback covers non-canonical numbers. Do not generate validation logic that rejects anything other than `111` / `222` / `333`.
|
|
291
278
|
- **Seed-based daily endpoints are DETERMINISTIC per `(seed, date)` pair.** Same seed plus same date returns the same reading. This is by design for push-notification consistency. Do not describe it as "cached" or retry on stale responses.
|
|
292
|
-
- **Timezone affects Western calculations more than Vedic.** Western natal charts must respect DST at time of birth. Vedic endpoints default to IST (`5.5`) which is DST-free.
|
|
279
|
+
- **Timezone affects Western calculations more than Vedic.** Western natal charts must respect DST at the time of birth. Vedic endpoints default to IST (`5.5`) which is DST-free. Pass the IANA `timezone` string from the location lookup (`"America/New_York"`, `"Asia/Kolkata"`) directly to chart calls and the server resolves the DST-correct offset for the chart `date`. Never pass the `utcOffset` of the city record (the offset in force today) or the clock of the user right now.
|
|
280
|
+
|
|
281
|
+
## Error handling
|
|
282
|
+
|
|
283
|
+
Errors raise `RoxyAPIError` with `error` (message), `code` (machine-readable), `status_code`, `doc_url` (documentation for the code), `retry_after` (seconds from the `Retry-After` header, or `None`) and `issues` (the failing fields of a `validation_error`, otherwise `None`). `ErrorCode` is a string enum of every code below, so `e.code == ErrorCode.RATE_LIMIT_PER_MINUTE` works:
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
from roxy_sdk import create_roxy, ErrorCode, RoxyAPIError
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
result = roxy.astrology.get_daily_horoscope(sign="invalid")
|
|
290
|
+
except RoxyAPIError as e:
|
|
291
|
+
if e.code == ErrorCode.VALIDATION_ERROR:
|
|
292
|
+
for issue in e.issues or []:
|
|
293
|
+
print(issue["path"], issue["message"], issue["code"]) # path is the dot-separated field, e.g. "time" or "person1.date"
|
|
294
|
+
print(e.code) # "validation_error"
|
|
295
|
+
print(e.status_code) # 400
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
<!-- BEGIN:ERRORS -->
|
|
299
|
+
Every error returns `{ error, code, doc_url }`. Switch on `code`; `error` is readable text and may change. Each meaning below says whether and when to retry, and `Retry-After` carries the wait when the response sends it. Every code is documented at https://roxyapi.com/docs/errors.
|
|
300
|
+
|
|
301
|
+
| Status | Code | Meaning |
|
|
302
|
+
|--------|------|---------|
|
|
303
|
+
| 400 | `validation_error` | A field is missing or invalid. The body lists every failing field at once in issues, each with its path and the expected shape. |
|
|
304
|
+
| 400 | `bad_request` | The request was rejected as a whole, for example a body that is not valid JSON or a date range that ends before it starts. |
|
|
305
|
+
| 400 | `date_out_of_range` | A date lies outside the span the planetary positions are served for. The error names the supported dates. |
|
|
306
|
+
| 401 | `api_key_required` | No API key was sent. |
|
|
307
|
+
| 401 | `invalid_api_key` | The key sent is malformed or cannot be verified. |
|
|
308
|
+
| 401 | `api_key_revoked` | The key was valid once and has since been revoked from the account page. |
|
|
309
|
+
| 401 | `subscription_not_found` | The key verifies, but the subscription behind it no longer exists. |
|
|
310
|
+
| 401 | `subscription_inactive` | The subscription behind the key is cancelled, expired or suspended, or a trial is past its end date. |
|
|
311
|
+
| 401 | `publishable_key_not_allowed_on_mcp` | A publishable key was used on a Remote MCP server, which takes a secret key. |
|
|
312
|
+
| 401 | `invalid_client_ip` | A keyless request could not be attributed to a caller. |
|
|
313
|
+
| 401 | `unauthorized` | Authentication failed with no more specific reason to give. |
|
|
314
|
+
| 403 | `forbidden` | The key is valid, but this request is not permitted. |
|
|
315
|
+
| 403 | `origin_required` | A publishable key with an origin allowlist was used with no Origin header, as from a server or a native app. |
|
|
316
|
+
| 403 | `origin_not_allowed` | The calling site is not on the origin allowlist of this publishable key. |
|
|
317
|
+
| 404 | `not_found` | Nothing is served at this path, or the identifier in it does not exist. A path miss adds suggestions, the closest real endpoints. |
|
|
318
|
+
| 405 | `method_not_allowed` | The path exists for a different HTTP method. The body lists the accepted methods in allow, as does the Allow header. |
|
|
319
|
+
| 409 | `conflict` | The request cannot be applied in the current state. |
|
|
320
|
+
| 410 | `gone` | The path was removed permanently. |
|
|
321
|
+
| 413 | `payload_too_large` | The request body is larger than the API accepts. A body far over the cap is refused before it reaches the API and returns a bare 413 with no JSON body. |
|
|
322
|
+
| 415 | `unsupported_media_type` | A request with a body was sent without Content-Type: application/json. |
|
|
323
|
+
| 422 | `unprocessable_entity` | Every field is valid, but the combination cannot be computed. |
|
|
324
|
+
| 429 | `rate_limit_exceeded` | The monthly request allowance is used up. Retry-After gives the seconds to the reset. |
|
|
325
|
+
| 429 | `rate_limit_per_minute` | The per-minute limit of the plan was reached. Retry-After gives the seconds to the next minute, and the monthly allowance is not charged. |
|
|
326
|
+
| 429 | `rate_limited` | A rate limit was reached, reported without the plan detail. |
|
|
327
|
+
| 429 | `free_tier_exhausted` | A keyless request has used up its trial allowance. |
|
|
328
|
+
| 500 | `internal_error` | The request failed inside our service. Retry with exponential backoff. |
|
|
329
|
+
| 503 | `compute_saturated` | Heavy calculations are briefly at capacity. Send the same request again after Retry-After seconds. |
|
|
330
|
+
| Any | `error` | A fallback for a status with no more specific code. The status and the error field carry the signal. |
|
|
331
|
+
<!-- END:ERRORS -->
|
|
332
|
+
|
|
333
|
+
### Retries
|
|
334
|
+
|
|
335
|
+
The client retries twice by default: HTTP 429, 500, 502, 503 and 504, plus connection failures where the request was never sent. Pass `max_retries` to change the count, or `max_retries=0` to turn retries off. Waits use the `Retry-After` header when present, otherwise jittered exponential backoff from `initial_delay` (default 0.5 seconds) up to `max_delay` (default 30 seconds). A `Retry-After` longer than `max_delay`, such as the one on `rate_limit_exceeded`, is not waited out: the error is raised with `retry_after` set. A retry never sleeps past `timeout`. Read timeouts are never retried.
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from roxy_sdk import create_roxy, RoxyAPIError
|
|
339
|
+
|
|
340
|
+
roxy = create_roxy("your-api-key", max_retries=4)
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
result = roxy.astrology.get_daily_horoscope(sign="aries")
|
|
344
|
+
except RoxyAPIError as e:
|
|
345
|
+
print(e.code) # "rate_limit_per_minute", "compute_saturated", "validation_error", ...
|
|
346
|
+
print(e.doc_url) # documentation page for that code
|
|
347
|
+
print(e.retry_after) # seconds the server asked you to wait, or None
|
|
348
|
+
```
|
|
293
349
|
|
|
294
350
|
## MCP equivalents
|
|
295
351
|
|
|
@@ -311,12 +367,27 @@ Use the SDK for typed Python apps. Use MCP for AI agents (Claude Desktop, Cursor
|
|
|
311
367
|
- **Date format is `YYYY-MM-DD`, time is `HH:MM:SS`.** Both are strings.
|
|
312
368
|
- **Western `timezone` is required** as an IANA string (`"Asia/Kolkata"`, `"America/New_York"`, `"Europe/London"`, `"UTC"`); the server resolves it to the DST-correct offset for the chart date. Vedic endpoints accept an optional `timezone` (same form) that defaults to IST when omitted. The decimal-number form (`5.5`) is also accepted by the API, but only inside `person1`/`person2` dicts - the top-level `timezone=` kwarg is typed `str`.
|
|
313
369
|
- **Errors raise `RoxyAPIError`.** Catch it and check `e.code`, `e.error`, and `e.status_code`.
|
|
314
|
-
- **
|
|
370
|
+
- **A `validation_error` carries `issues`, each with `path` (the dot-separated field, such as `time` or `person1.date`), `message` and `code`.** Loop over them to show every failing field:
|
|
371
|
+
```python
|
|
372
|
+
except RoxyAPIError as e:
|
|
373
|
+
for issue in e.issues or []:
|
|
374
|
+
print(issue["path"], issue["message"], issue["code"])
|
|
375
|
+
```
|
|
376
|
+
- **Switch on `e.code`, not `e.error`.** The message may change; the code is stable.
|
|
377
|
+
- **A city record has `city`, `province`, `country`, `iso2`, `latitude`, `longitude`, `timezone`, `utcOffset` and `population`.** The name is `city`, never `name`.
|
|
378
|
+
- **`utcOffset` on a city record is the offset in force today.** Pass the IANA `timezone` string to chart calls so the server resolves the offset for the birth date.
|
|
379
|
+
- **A natal chart `planets` is a list of objects.** Find a body by `name`: `next(p for p in chart["planets"] if p["name"] == "Moon")`.
|
|
380
|
+
- **In a Vedic chart `meta.Moon.rashi` is a string and `meta.Moon.nakshatra` is an object** (`name`, `pada`, `lord`). Display `kundli["meta"]["Moon"]["nakshatra"]["name"]`.
|
|
381
|
+
- **`sign` is matched case-insensitively.** Lowercase is canonical.
|
|
382
|
+
- **Total requests on a persistent failure = 1 + retries.** The default `max_retries` of 2 makes 3 requests; `max_retries=0` makes exactly one.
|
|
315
383
|
- **List endpoints return a paginated envelope**, `{"total": ..., "limit": ..., "offset": ...}` plus a named list (`cities`, `crystals`, `hexagrams`, `symbols`), never a bare list. Pass `limit=` to widen a page; `list_hexagrams` defaults to 20 of 64.
|
|
316
384
|
|
|
317
385
|
## Links
|
|
318
386
|
|
|
387
|
+
- Documentation: https://roxyapi.com/docs
|
|
319
388
|
- Interactive API docs: https://roxyapi.com/api-reference
|
|
320
389
|
- Pricing and API keys: https://roxyapi.com/pricing
|
|
321
390
|
- MCP for AI agents: https://roxyapi.com/docs/mcp
|
|
322
|
-
-
|
|
391
|
+
- Templates: https://roxyapi.com/templates
|
|
392
|
+
- Source and issues: https://github.com/RoxyAPI/sdk-python
|
|
393
|
+
- Other SDKs: TypeScript https://www.npmjs.com/package/@roxyapi/sdk, PHP https://packagist.org/packages/roxyapi/sdk, .NET https://www.nuget.org/packages/RoxyApi.Sdk, Go https://pkg.go.dev/github.com/RoxyAPI/sdk-go, Rust https://crates.io/crates/roxyapi
|
|
@@ -4,6 +4,13 @@ Regenerate with: python generate.py
|
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import asyncio
|
|
8
|
+
import math
|
|
9
|
+
import random
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from email.utils import parsedate_to_datetime
|
|
13
|
+
from enum import Enum
|
|
7
14
|
from typing import Any
|
|
8
15
|
|
|
9
16
|
import httpx
|
|
@@ -14,17 +21,107 @@ from roxy_sdk.version import VERSION
|
|
|
14
21
|
_BASE_URL = "https://roxyapi.com/api/v2"
|
|
15
22
|
|
|
16
23
|
|
|
24
|
+
class ErrorCode(str, Enum):
|
|
25
|
+
"""Every `code` the API returns. Compare with `RoxyAPIError.code`."""
|
|
26
|
+
|
|
27
|
+
VALIDATION_ERROR = "validation_error"
|
|
28
|
+
'400. A field is missing or invalid. The body lists every failing field at once in issues, each with its path and the expected shape.'
|
|
29
|
+
BAD_REQUEST = "bad_request"
|
|
30
|
+
'400. The request was rejected as a whole, for example a body that is not valid JSON or a date range that ends before it starts.'
|
|
31
|
+
DATE_OUT_OF_RANGE = "date_out_of_range"
|
|
32
|
+
'400. A date lies outside the span the planetary positions are served for. The error names the supported dates.'
|
|
33
|
+
API_KEY_REQUIRED = "api_key_required"
|
|
34
|
+
'401. No API key was sent.'
|
|
35
|
+
INVALID_API_KEY = "invalid_api_key"
|
|
36
|
+
'401. The key sent is malformed or cannot be verified.'
|
|
37
|
+
API_KEY_REVOKED = "api_key_revoked"
|
|
38
|
+
'401. The key was valid once and has since been revoked from the account page.'
|
|
39
|
+
SUBSCRIPTION_NOT_FOUND = "subscription_not_found"
|
|
40
|
+
'401. The key verifies, but the subscription behind it no longer exists.'
|
|
41
|
+
SUBSCRIPTION_INACTIVE = "subscription_inactive"
|
|
42
|
+
'401. The subscription behind the key is cancelled, expired or suspended, or a trial is past its end date.'
|
|
43
|
+
PUBLISHABLE_KEY_NOT_ALLOWED_ON_MCP = "publishable_key_not_allowed_on_mcp"
|
|
44
|
+
'401. A publishable key was used on a Remote MCP server, which takes a secret key.'
|
|
45
|
+
INVALID_CLIENT_IP = "invalid_client_ip"
|
|
46
|
+
'401. A keyless request could not be attributed to a caller.'
|
|
47
|
+
UNAUTHORIZED = "unauthorized"
|
|
48
|
+
'401. Authentication failed with no more specific reason to give.'
|
|
49
|
+
FORBIDDEN = "forbidden"
|
|
50
|
+
'403. The key is valid, but this request is not permitted.'
|
|
51
|
+
ORIGIN_REQUIRED = "origin_required"
|
|
52
|
+
'403. A publishable key with an origin allowlist was used with no Origin header, as from a server or a native app.'
|
|
53
|
+
ORIGIN_NOT_ALLOWED = "origin_not_allowed"
|
|
54
|
+
'403. The calling site is not on the origin allowlist of this publishable key.'
|
|
55
|
+
NOT_FOUND = "not_found"
|
|
56
|
+
'404. Nothing is served at this path, or the identifier in it does not exist. A path miss adds suggestions, the closest real endpoints.'
|
|
57
|
+
METHOD_NOT_ALLOWED = "method_not_allowed"
|
|
58
|
+
'405. The path exists for a different HTTP method. The body lists the accepted methods in allow, as does the Allow header.'
|
|
59
|
+
CONFLICT = "conflict"
|
|
60
|
+
'409. The request cannot be applied in the current state.'
|
|
61
|
+
GONE = "gone"
|
|
62
|
+
'410. The path was removed permanently.'
|
|
63
|
+
PAYLOAD_TOO_LARGE = "payload_too_large"
|
|
64
|
+
'413. The request body is larger than the API accepts. A body far over the cap is refused before it reaches the API and returns a bare 413 with no JSON body.'
|
|
65
|
+
UNSUPPORTED_MEDIA_TYPE = "unsupported_media_type"
|
|
66
|
+
'415. A request with a body was sent without Content-Type: application/json.'
|
|
67
|
+
UNPROCESSABLE_ENTITY = "unprocessable_entity"
|
|
68
|
+
'422. Every field is valid, but the combination cannot be computed.'
|
|
69
|
+
RATE_LIMIT_EXCEEDED = "rate_limit_exceeded"
|
|
70
|
+
'429. The monthly request allowance is used up. Retry-After gives the seconds to the reset.'
|
|
71
|
+
RATE_LIMIT_PER_MINUTE = "rate_limit_per_minute"
|
|
72
|
+
'429. The per-minute limit of the plan was reached. Retry-After gives the seconds to the next minute, and the monthly allowance is not charged.'
|
|
73
|
+
RATE_LIMITED = "rate_limited"
|
|
74
|
+
'429. A rate limit was reached, reported without the plan detail.'
|
|
75
|
+
FREE_TIER_EXHAUSTED = "free_tier_exhausted"
|
|
76
|
+
'429. A keyless request has used up its trial allowance.'
|
|
77
|
+
INTERNAL_ERROR = "internal_error"
|
|
78
|
+
'500. The request failed inside our service. Retry with exponential backoff.'
|
|
79
|
+
COMPUTE_SATURATED = "compute_saturated"
|
|
80
|
+
'503. Heavy calculations are briefly at capacity. Send the same request again after Retry-After seconds.'
|
|
81
|
+
ERROR = "error"
|
|
82
|
+
'Any. A fallback for a status with no more specific code. The status and the error field carry the signal.'
|
|
83
|
+
|
|
84
|
+
def __str__(self) -> str:
|
|
85
|
+
return str(self.value)
|
|
86
|
+
|
|
87
|
+
|
|
17
88
|
class RoxyAPIError(Exception):
|
|
18
|
-
"""Error returned by the RoxyAPI.
|
|
89
|
+
"""Error returned by the RoxyAPI.
|
|
90
|
+
|
|
91
|
+
Switch on `code`. `doc_url` links the documentation for that code. `retry_after` is the
|
|
92
|
+
wait in seconds the server asked for, or None when the response sent no Retry-After.
|
|
93
|
+
On a validation_error, `issues` lists every failing field, each with `path`, `message`
|
|
94
|
+
and `code`; it is None for any other error.
|
|
95
|
+
"""
|
|
19
96
|
|
|
20
|
-
def __init__(
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
error: str,
|
|
100
|
+
code: str,
|
|
101
|
+
status_code: int,
|
|
102
|
+
*,
|
|
103
|
+
doc_url: str | None = None,
|
|
104
|
+
retry_after: float | None = None,
|
|
105
|
+
issues: list[dict[str, Any]] | None = None,
|
|
106
|
+
) -> None:
|
|
21
107
|
self.error = error
|
|
22
108
|
self.code = code
|
|
23
109
|
self.status_code = status_code
|
|
110
|
+
self.doc_url = doc_url
|
|
111
|
+
self.retry_after = retry_after
|
|
112
|
+
self.issues = issues
|
|
24
113
|
super().__init__(f"[{status_code}] {code}: {error}")
|
|
25
114
|
|
|
26
115
|
def __repr__(self) -> str:
|
|
27
|
-
return
|
|
116
|
+
return (
|
|
117
|
+
f"RoxyAPIError(error={self.error!r}, code={self.code!r}, "
|
|
118
|
+
f"status_code={self.status_code}, doc_url={self.doc_url!r}, "
|
|
119
|
+
f"retry_after={self.retry_after!r}, issues={self.issues!r})"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
_RETRY_STATUSES = frozenset({429, 500, 502, 503, 504})
|
|
124
|
+
_NEVER_SENT = (httpx.ConnectError, httpx.ConnectTimeout)
|
|
28
125
|
|
|
29
126
|
|
|
30
127
|
def _default_headers(api_key: str) -> dict[str, str]:
|
|
@@ -35,6 +132,49 @@ def _default_headers(api_key: str) -> dict[str, str]:
|
|
|
35
132
|
}
|
|
36
133
|
|
|
37
134
|
|
|
135
|
+
def _parse_retry_after(value: str | None) -> float | None:
|
|
136
|
+
"""Retry-After as seconds, from either delta-seconds or an HTTP-date."""
|
|
137
|
+
if value is None:
|
|
138
|
+
return None
|
|
139
|
+
try:
|
|
140
|
+
seconds = float(value)
|
|
141
|
+
except ValueError:
|
|
142
|
+
try:
|
|
143
|
+
when = parsedate_to_datetime(value)
|
|
144
|
+
except (TypeError, ValueError):
|
|
145
|
+
return None
|
|
146
|
+
if when.tzinfo is None:
|
|
147
|
+
when = when.replace(tzinfo=timezone.utc)
|
|
148
|
+
seconds = (when - datetime.now(timezone.utc)).total_seconds()
|
|
149
|
+
return None if math.isnan(seconds) else max(seconds, 0.0)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _Retry:
|
|
153
|
+
"""Retry policy: HTTP 429, 500, 502, 503, 504 and connection failures only."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, max_retries: int, initial_delay: float, max_delay: float, timeout: float) -> None:
|
|
156
|
+
if max_retries < 0:
|
|
157
|
+
raise ValueError("max_retries must be 0 or greater")
|
|
158
|
+
self.max_retries = max_retries
|
|
159
|
+
self.initial_delay = initial_delay
|
|
160
|
+
self.max_delay = max_delay
|
|
161
|
+
self.timeout = timeout
|
|
162
|
+
|
|
163
|
+
def delay(self, retry_after: float | None, attempt: int, started: float) -> float | None:
|
|
164
|
+
"""Seconds to sleep before the next attempt, or None to surface the error now."""
|
|
165
|
+
if attempt >= self.max_retries:
|
|
166
|
+
return None
|
|
167
|
+
if retry_after is not None:
|
|
168
|
+
if retry_after > self.max_delay:
|
|
169
|
+
return None
|
|
170
|
+
wait = min(self.max_delay, retry_after + random.uniform(0, self.initial_delay))
|
|
171
|
+
else:
|
|
172
|
+
wait = random.uniform(0, min(self.max_delay, self.initial_delay * 2**attempt))
|
|
173
|
+
if time.monotonic() - started + wait >= self.timeout:
|
|
174
|
+
return None
|
|
175
|
+
return wait
|
|
176
|
+
|
|
177
|
+
|
|
38
178
|
def _handle_response(resp: httpx.Response) -> Any:
|
|
39
179
|
"""Parse response, raising RoxyAPIError on 4xx/5xx."""
|
|
40
180
|
if resp.status_code >= 400:
|
|
@@ -42,10 +182,15 @@ def _handle_response(resp: httpx.Response) -> Any:
|
|
|
42
182
|
body = resp.json()
|
|
43
183
|
except Exception:
|
|
44
184
|
body = {}
|
|
185
|
+
if not isinstance(body, dict):
|
|
186
|
+
body = {}
|
|
45
187
|
raise RoxyAPIError(
|
|
46
188
|
error=body.get("error", resp.text or "Unknown error"),
|
|
47
189
|
code=body.get("code", "unknown"),
|
|
48
190
|
status_code=resp.status_code,
|
|
191
|
+
doc_url=body.get("doc_url"),
|
|
192
|
+
retry_after=_parse_retry_after(resp.headers.get("Retry-After")),
|
|
193
|
+
issues=body.get("issues") if isinstance(body.get("issues"), list) else None,
|
|
49
194
|
)
|
|
50
195
|
return resp.json()
|
|
51
196
|
|
|
@@ -53,21 +198,60 @@ def _handle_response(resp: httpx.Response) -> Any:
|
|
|
53
198
|
class _BaseDomain:
|
|
54
199
|
"""Base class for all domain namespaces."""
|
|
55
200
|
|
|
56
|
-
def __init__(self, client: httpx.Client, async_client: httpx.AsyncClient) -> None:
|
|
201
|
+
def __init__(self, client: httpx.Client, async_client: httpx.AsyncClient, retry: _Retry) -> None:
|
|
57
202
|
self._client = client
|
|
58
203
|
self._async_client = async_client
|
|
204
|
+
self._retry = retry
|
|
205
|
+
|
|
206
|
+
def _request(self, method: str, path: str, body: dict[str, Any] | None, params: dict[str, Any] | None) -> Any:
|
|
207
|
+
started = time.monotonic()
|
|
208
|
+
attempt = 0
|
|
209
|
+
while True:
|
|
210
|
+
try:
|
|
211
|
+
resp = self._client.request(method, path, json=body, params=params)
|
|
212
|
+
except _NEVER_SENT:
|
|
213
|
+
wait = self._retry.delay(None, attempt, started)
|
|
214
|
+
if wait is None:
|
|
215
|
+
raise
|
|
216
|
+
else:
|
|
217
|
+
if resp.status_code not in _RETRY_STATUSES:
|
|
218
|
+
return _handle_response(resp)
|
|
219
|
+
wait = self._retry.delay(_parse_retry_after(resp.headers.get("Retry-After")), attempt, started)
|
|
220
|
+
if wait is None:
|
|
221
|
+
return _handle_response(resp)
|
|
222
|
+
time.sleep(wait)
|
|
223
|
+
attempt += 1
|
|
224
|
+
|
|
225
|
+
async def _request_async(self, method: str, path: str, body: dict[str, Any] | None, params: dict[str, Any] | None) -> Any:
|
|
226
|
+
started = time.monotonic()
|
|
227
|
+
attempt = 0
|
|
228
|
+
while True:
|
|
229
|
+
try:
|
|
230
|
+
resp = await self._async_client.request(method, path, json=body, params=params)
|
|
231
|
+
except _NEVER_SENT:
|
|
232
|
+
wait = self._retry.delay(None, attempt, started)
|
|
233
|
+
if wait is None:
|
|
234
|
+
raise
|
|
235
|
+
else:
|
|
236
|
+
if resp.status_code not in _RETRY_STATUSES:
|
|
237
|
+
return _handle_response(resp)
|
|
238
|
+
wait = self._retry.delay(_parse_retry_after(resp.headers.get("Retry-After")), attempt, started)
|
|
239
|
+
if wait is None:
|
|
240
|
+
return _handle_response(resp)
|
|
241
|
+
await asyncio.sleep(wait)
|
|
242
|
+
attempt += 1
|
|
59
243
|
|
|
60
244
|
def _get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
61
|
-
return
|
|
245
|
+
return self._request("GET", path, None, params)
|
|
62
246
|
|
|
63
247
|
async def _get_async(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
64
|
-
return
|
|
248
|
+
return await self._request_async("GET", path, None, params)
|
|
65
249
|
|
|
66
250
|
def _post(self, path: str, body: dict[str, Any] | None = None, params: dict[str, Any] | None = None) -> Any:
|
|
67
|
-
return
|
|
251
|
+
return self._request("POST", path, body, params)
|
|
68
252
|
|
|
69
253
|
async def _post_async(self, path: str, body: dict[str, Any] | None = None, params: dict[str, Any] | None = None) -> Any:
|
|
70
|
-
return
|
|
254
|
+
return await self._request_async("POST", path, body, params)
|
|
71
255
|
|
|
72
256
|
|
|
73
257
|
class AstrologyDomain(_BaseDomain):
|
|
@@ -6895,32 +7079,42 @@ class Roxy:
|
|
|
6895
7079
|
horoscope = roxy.astrology.get_daily_horoscope(sign="aries")
|
|
6896
7080
|
"""
|
|
6897
7081
|
|
|
6898
|
-
def __init__(
|
|
7082
|
+
def __init__(
|
|
7083
|
+
self,
|
|
7084
|
+
api_key: str,
|
|
7085
|
+
base_url: str = _BASE_URL,
|
|
7086
|
+
timeout: float = 30.0,
|
|
7087
|
+
*,
|
|
7088
|
+
max_retries: int = 2,
|
|
7089
|
+
initial_delay: float = 0.5,
|
|
7090
|
+
max_delay: float = 30.0,
|
|
7091
|
+
) -> None:
|
|
6899
7092
|
if not api_key:
|
|
6900
7093
|
raise ValueError("API key is required. Get one at https://roxyapi.com/pricing")
|
|
7094
|
+
retry = _Retry(max_retries, initial_delay, max_delay, timeout)
|
|
6901
7095
|
headers = _default_headers(api_key)
|
|
6902
7096
|
self._client = httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
|
|
6903
7097
|
self._async_client = httpx.AsyncClient(base_url=base_url, headers=headers, timeout=timeout)
|
|
6904
|
-
self.astrology = AstrologyDomain(self._client, self._async_client)
|
|
6905
|
-
self.vedic_astrology = VedicAstrologyDomain(self._client, self._async_client)
|
|
6906
|
-
self.forecast = ForecastDomain(self._client, self._async_client)
|
|
6907
|
-
self.human_design = HumanDesignDomain(self._client, self._async_client)
|
|
6908
|
-
self.chinese_astrology = ChineseAstrologyDomain(self._client, self._async_client)
|
|
6909
|
-
self.feng_shui = FengShuiDomain(self._client, self._async_client)
|
|
6910
|
-
self.mesoamerican_astrology = MesoamericanAstrologyDomain(self._client, self._async_client)
|
|
6911
|
-
self.vastu = VastuDomain(self._client, self._async_client)
|
|
6912
|
-
self.numerology = NumerologyDomain(self._client, self._async_client)
|
|
6913
|
-
self.kabbalah = KabbalahDomain(self._client, self._async_client)
|
|
6914
|
-
self.tarot = TarotDomain(self._client, self._async_client)
|
|
6915
|
-
self.biorhythm = BiorhythmDomain(self._client, self._async_client)
|
|
6916
|
-
self.ayurveda = AyurvedaDomain(self._client, self._async_client)
|
|
6917
|
-
self.iching = IchingDomain(self._client, self._async_client)
|
|
6918
|
-
self.crystals = CrystalsDomain(self._client, self._async_client)
|
|
6919
|
-
self.dreams = DreamsDomain(self._client, self._async_client)
|
|
6920
|
-
self.angel_numbers = AngelNumbersDomain(self._client, self._async_client)
|
|
6921
|
-
self.location = LocationDomain(self._client, self._async_client)
|
|
6922
|
-
self.usage = UsageDomain(self._client, self._async_client)
|
|
6923
|
-
self.languages = LanguagesDomain(self._client, self._async_client)
|
|
7098
|
+
self.astrology = AstrologyDomain(self._client, self._async_client, retry)
|
|
7099
|
+
self.vedic_astrology = VedicAstrologyDomain(self._client, self._async_client, retry)
|
|
7100
|
+
self.forecast = ForecastDomain(self._client, self._async_client, retry)
|
|
7101
|
+
self.human_design = HumanDesignDomain(self._client, self._async_client, retry)
|
|
7102
|
+
self.chinese_astrology = ChineseAstrologyDomain(self._client, self._async_client, retry)
|
|
7103
|
+
self.feng_shui = FengShuiDomain(self._client, self._async_client, retry)
|
|
7104
|
+
self.mesoamerican_astrology = MesoamericanAstrologyDomain(self._client, self._async_client, retry)
|
|
7105
|
+
self.vastu = VastuDomain(self._client, self._async_client, retry)
|
|
7106
|
+
self.numerology = NumerologyDomain(self._client, self._async_client, retry)
|
|
7107
|
+
self.kabbalah = KabbalahDomain(self._client, self._async_client, retry)
|
|
7108
|
+
self.tarot = TarotDomain(self._client, self._async_client, retry)
|
|
7109
|
+
self.biorhythm = BiorhythmDomain(self._client, self._async_client, retry)
|
|
7110
|
+
self.ayurveda = AyurvedaDomain(self._client, self._async_client, retry)
|
|
7111
|
+
self.iching = IchingDomain(self._client, self._async_client, retry)
|
|
7112
|
+
self.crystals = CrystalsDomain(self._client, self._async_client, retry)
|
|
7113
|
+
self.dreams = DreamsDomain(self._client, self._async_client, retry)
|
|
7114
|
+
self.angel_numbers = AngelNumbersDomain(self._client, self._async_client, retry)
|
|
7115
|
+
self.location = LocationDomain(self._client, self._async_client, retry)
|
|
7116
|
+
self.usage = UsageDomain(self._client, self._async_client, retry)
|
|
7117
|
+
self.languages = LanguagesDomain(self._client, self._async_client, retry)
|
|
6924
7118
|
|
|
6925
7119
|
def close(self) -> None:
|
|
6926
7120
|
"""Close sync HTTP connections."""
|
|
@@ -6947,13 +7141,31 @@ class Roxy:
|
|
|
6947
7141
|
return f"Roxy(base_url={self._client.base_url!r})"
|
|
6948
7142
|
|
|
6949
7143
|
|
|
6950
|
-
def create_roxy(
|
|
7144
|
+
def create_roxy(
|
|
7145
|
+
api_key: str,
|
|
7146
|
+
base_url: str = _BASE_URL,
|
|
7147
|
+
timeout: float = 30.0,
|
|
7148
|
+
*,
|
|
7149
|
+
max_retries: int = 2,
|
|
7150
|
+
initial_delay: float = 0.5,
|
|
7151
|
+
max_delay: float = 30.0,
|
|
7152
|
+
) -> Roxy:
|
|
6951
7153
|
"""
|
|
6952
7154
|
Create a configured Roxy instance.
|
|
6953
7155
|
|
|
6954
7156
|
Args:
|
|
6955
7157
|
api_key: Your RoxyAPI key. Get one at https://roxyapi.com/pricing
|
|
6956
7158
|
base_url: Override the default API base URL. Default: https://roxyapi.com/api/v2
|
|
6957
|
-
timeout: Request timeout in seconds. Default: 30.
|
|
7159
|
+
timeout: Request timeout in seconds, also the budget a retry never sleeps past. Default: 30.
|
|
7160
|
+
max_retries: Retries on 429, 500, 502, 503, 504 and connection failures. Default: 2; 0 turns retries off.
|
|
7161
|
+
initial_delay: Base backoff in seconds. Default: 0.5.
|
|
7162
|
+
max_delay: Longest wait in seconds; a longer Retry-After is surfaced, not waited. Default: 30.
|
|
6958
7163
|
"""
|
|
6959
|
-
return Roxy(
|
|
7164
|
+
return Roxy(
|
|
7165
|
+
api_key=api_key,
|
|
7166
|
+
base_url=base_url,
|
|
7167
|
+
timeout=timeout,
|
|
7168
|
+
max_retries=max_retries,
|
|
7169
|
+
initial_delay=initial_delay,
|
|
7170
|
+
max_delay=max_delay,
|
|
7171
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "1.2.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = "1.1.8"
|
|
File without changes
|
|
File without changes
|