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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roxy-sdk
3
- Version: 1.1.8
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
- ## Quickstart
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
- Get your API key at [roxyapi.com/pricing](https://roxyapi.com/pricing). Free test keys available on the [interactive docs](https://roxyapi.com/api-reference).
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, Cursor, Copilot, Codex, Gemini CLI)
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
- ## Async support
465
+ ## Authentication
466
466
 
467
- Every method has an `_async` suffix variant for use with asyncio:
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 asyncio
472
+ import os
471
473
  from roxy_sdk import create_roxy
472
474
 
473
- async def main():
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
- - [API Documentation](https://roxyapi.com/docs)
607
- - [Interactive API Reference](https://roxyapi.com/api-reference)
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 Agents](https://roxyapi.com/docs/mcp)
654
+ - [MCP setup for AI agents](https://roxyapi.com/docs/mcp)
610
655
  - [Templates](https://roxyapi.com/templates)
611
- - [TypeScript SDK](https://www.npmjs.com/package/@roxyapi/sdk)
612
- - [Rust SDK](https://crates.io/crates/roxyapi)
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
- ## Quickstart
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
- Get your API key at [roxyapi.com/pricing](https://roxyapi.com/pricing). Free test keys available on the [interactive docs](https://roxyapi.com/api-reference).
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, Cursor, Copilot, Codex, Gemini CLI)
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
- ## Async support
442
+ ## Authentication
443
443
 
444
- Every method has an `_async` suffix variant for use with asyncio:
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 asyncio
449
+ import os
448
450
  from roxy_sdk import create_roxy
449
451
 
450
- async def main():
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
- - [API Documentation](https://roxyapi.com/docs)
584
- - [Interactive API Reference](https://roxyapi.com/api-reference)
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 Agents](https://roxyapi.com/docs/mcp)
631
+ - [MCP setup for AI agents](https://roxyapi.com/docs/mcp)
587
632
  - [Templates](https://roxyapi.com/templates)
588
- - [TypeScript SDK](https://www.npmjs.com/package/@roxyapi/sdk)
589
- - [Rust SDK](https://crates.io/crates/roxyapi)
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
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "roxy-sdk"
7
- version = "1.1.8"
7
+ version = "1.2.0"
8
8
  description = "Python SDK for RoxyAPI. Astrology, tarot, numerology, and more."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "roxy-sdk"
7
- version = "1.1.8"
7
+ version = "1.2.0"
8
8
  description = "Python SDK for RoxyAPI. Astrology, tarot, numerology, and more."
9
9
  readme = "README.md"
10
10
  license = "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 request itself,
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
- # If you prefer numbers, city["utcOffset"] (decimal: 5.5, -5, ...) also works.
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", time="14:30:00",
85
- latitude=city["latitude"], longitude=city["longitude"],
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", time="14:30:00",
116
- latitude=40.7128, longitude=-74.006, timezone="America/New_York",
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", time="14:30:00",
121
- latitude=28.6139, longitude=77.209,
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", year=1990, month=1, day=15,
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"` | `"Aries"`, `"♈"`, `"1"`, `"ARIES"` (case-insensitive but prefer lowercase) |
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"` (smoothed mean node, traditional Vedic default) or `"true"` (osculating node with perturbation corrections) to select which calculation to use.
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. Use `utcOffset` from the Location API response as your `timezone` kwarg, not whatever clock the user is in right now.
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
- - **Switch on `code`, not `error`.** Codes are stable. Messages may change.
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
- - TypeScript SDK: https://www.npmjs.com/package/@roxyapi/sdk
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
@@ -0,0 +1,7 @@
1
+ """RoxyAPI Python SDK."""
2
+
3
+ from roxy_sdk.factory import ErrorCode, Roxy, RoxyAPIError, create_roxy
4
+ from roxy_sdk.version import VERSION
5
+
6
+ __version__ = VERSION
7
+ __all__ = ["ErrorCode", "Roxy", "RoxyAPIError", "create_roxy", "__version__"]
@@ -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__(self, error: str, code: str, status_code: int) -> None:
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 f"RoxyAPIError(error={self.error!r}, code={self.code!r}, status_code={self.status_code})"
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 _handle_response(self._client.get(path, params=params))
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 _handle_response(await self._async_client.get(path, params=params))
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 _handle_response(self._client.post(path, json=body, params=params))
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 _handle_response(await self._async_client.post(path, json=body, params=params))
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__(self, api_key: str, base_url: str = _BASE_URL, timeout: float = 30.0) -> None:
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(api_key: str, base_url: str = _BASE_URL, timeout: float = 30.0) -> 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(api_key=api_key, base_url=base_url, timeout=timeout)
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,7 +0,0 @@
1
- """RoxyAPI Python SDK."""
2
-
3
- from roxy_sdk.factory import Roxy, RoxyAPIError, create_roxy
4
- from roxy_sdk.version import VERSION
5
-
6
- __version__ = VERSION
7
- __all__ = ["Roxy", "RoxyAPIError", "create_roxy", "__version__"]
@@ -1 +0,0 @@
1
- VERSION = "1.1.8"
File without changes
File without changes