ritten-python-sdk 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ritten/__init__.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ Ritten SDK.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ from importlib.metadata import version, PackageNotFoundError
9
+
10
+ from ritten.ritten import Ritten
11
+ from ritten.auth import Auth
12
+ from ritten.config import Config
13
+ from ritten.exceptions import (
14
+ RittenError,
15
+ RittenClientError,
16
+ RittenConnectionError,
17
+ RittenAPIError,
18
+ RittenAuthError,
19
+ RittenUnauthorizedError,
20
+ RittenValidationError,
21
+ RittenNotFoundError,
22
+ RittenRateLimitError,
23
+ RittenServerError,
24
+ )
25
+ from ritten.resources import (
26
+ Resource,
27
+ Calendar,
28
+ Cases,
29
+ Contacts,
30
+ Facilities,
31
+ Forms,
32
+ Insurance,
33
+ Organizations,
34
+ Patients,
35
+ Programs,
36
+ Users,
37
+ )
38
+ from ritten.storage import TokenStorage, MemoryStorage
39
+
40
+ __all__ = [
41
+ "Ritten",
42
+ "Auth",
43
+ "Config",
44
+ "TokenStorage",
45
+ "MemoryStorage",
46
+ "RittenError",
47
+ "RittenClientError",
48
+ "RittenConnectionError",
49
+ "RittenAPIError",
50
+ "RittenAuthError",
51
+ "RittenUnauthorizedError",
52
+ "RittenValidationError",
53
+ "RittenNotFoundError",
54
+ "RittenRateLimitError",
55
+ "RittenServerError",
56
+ "Resource",
57
+ "Calendar",
58
+ "Cases",
59
+ "Contacts",
60
+ "Facilities",
61
+ "Forms",
62
+ "Insurance",
63
+ "Organizations",
64
+ "Patients",
65
+ "Programs",
66
+ "Users",
67
+ ]
68
+
69
+
70
+ try:
71
+ __version__ = version("ritten-python-sdk")
72
+ except PackageNotFoundError:
73
+ __version__ = "unknown"
ritten/auth.py ADDED
@@ -0,0 +1,91 @@
1
+ """
2
+ Ritten SDK Authentication Handler.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from ritten.config import Config
10
+ from ritten.exceptions import RittenAuthError, RittenUnauthorizedError
11
+
12
+
13
+ class Auth(httpx.Auth):
14
+ """Authentication handler for the Ritten API."""
15
+
16
+ def __init__(self, config: Config):
17
+ self.config = config
18
+
19
+ @property
20
+ def access_token(self) -> str | None:
21
+ """Fetch the access token."""
22
+ token = self.config.storage.get_token()
23
+ return token
24
+
25
+ @access_token.setter
26
+ def access_token(self, value: str) -> Auth:
27
+ """Set a new access token."""
28
+ self.config.storage.set_token(value)
29
+
30
+ return self
31
+
32
+ @property
33
+ def client_id(self) -> str | None:
34
+ """Fetch the client ID for authentication."""
35
+ return self.config.client_id
36
+
37
+ @property
38
+ def client_secret(self) -> str | None:
39
+ """Fetch the client secret for authentication."""
40
+ return self.config.client_secret
41
+
42
+ def _fetch_new_token(self) -> None:
43
+ """Fetch a new access token using client credentials."""
44
+ if not self.config.client_id or not self.config.client_secret:
45
+ raise RittenAuthError(
46
+ "Client ID and Client Secret are required to fetch a new access token."
47
+ )
48
+
49
+ response = httpx.post(
50
+ f"{self.config.base_url}/oauth/token",
51
+ json={
52
+ "grant_type": "client_credentials",
53
+ "client_id": self.config.client_id,
54
+ "client_secret": self.config.client_secret,
55
+ "audience": self.config.audience,
56
+ },
57
+ )
58
+
59
+ if response.status_code == 200:
60
+ self.access_token = response.json()["access_token"]
61
+
62
+ def add_bearer_token(self, request: httpx.Request) -> httpx.Request:
63
+ """Add the Bearer token to the request headers."""
64
+ request.headers["Authorization"] = f"Bearer {self.access_token}"
65
+ return request
66
+
67
+ def auth_flow(self, request: httpx.Request):
68
+ """Controls the entire authentication workflow."""
69
+
70
+ if not self.access_token:
71
+ self._fetch_new_token()
72
+ self.add_bearer_token(request)
73
+
74
+ response = yield request
75
+
76
+ # Handle Expiration (401 Unauthorized)
77
+ if response.status_code == 401:
78
+ self._fetch_new_token()
79
+ self.add_bearer_token(request)
80
+
81
+ # replays the API call
82
+ retry_request = yield request
83
+
84
+ # if still fails
85
+ if retry_request.status_code == 401:
86
+ retry_request.read()
87
+ response = retry_request.json()
88
+ raise RittenUnauthorizedError(
89
+ message=response.get("message", "Unauthorized request."),
90
+ status_code=retry_request.status_code,
91
+ )
ritten/config.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Ritten SDK Configuration.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ from typing import Any
9
+ from pydantic import (
10
+ BaseModel,
11
+ Field,
12
+ field_validator,
13
+ ConfigDict,
14
+ )
15
+ from ritten.storage import TokenStorage, MemoryStorage
16
+ from ritten.exceptions import RittenError
17
+
18
+
19
+ class Config(BaseModel):
20
+ """Configuration for the Ritten SDK client."""
21
+
22
+ model_config = ConfigDict(arbitrary_types_allowed=True)
23
+
24
+ base_url: str = Field(
25
+ default="https://api.ritten.io/v1",
26
+ description="Base URL for the Ritten API.",
27
+ )
28
+
29
+ audience: str = Field(
30
+ default="https://external-api.ritten.io",
31
+ description="Audience for OAuth token requests.",
32
+ )
33
+
34
+ # Authentication
35
+ tenant_id: str | None = Field(
36
+ default="ritclinic",
37
+ description="Tenant ID for the Ritten API.",
38
+ )
39
+
40
+ client_id: str | None = Field(
41
+ default=None,
42
+ description="Client ID for authentication.",
43
+ )
44
+ client_secret: str | None = Field(
45
+ default=None,
46
+ description="Client secret for authentication.",
47
+ )
48
+
49
+ storage: TokenStorage = Field(
50
+ default=MemoryStorage(),
51
+ description="Token storage mechanism. Defaults to in-memory storage.",
52
+ )
53
+
54
+ @field_validator("storage", mode="after")
55
+ @classmethod
56
+ def validate_storage_interface(cls, value: Any) -> Any:
57
+ """Validates that the provided storage object implements the `TokenStorage` protocol if it is not None."""
58
+ if value is None:
59
+ return value
60
+
61
+ if not isinstance(value, TokenStorage):
62
+ raise RittenError(
63
+ "The provided storage object must implement the `TokenStorage` protocol."
64
+ )
65
+
66
+ return value
67
+
68
+ # Connection pooling and timeout settings
69
+ timeout: float = Field(
70
+ default=30.0,
71
+ description="The timeout in seconds for the HTTP client.",
72
+ )
73
+ max_connections: int = Field(
74
+ default=10,
75
+ description="Maximum number of concurrent connections in the pool.",
76
+ )
77
+ max_keepalive_connections: int = Field(
78
+ default=5,
79
+ description="Maximum number of keep-alive connections to maintain.",
80
+ )
81
+
82
+ keepalive_expiry: float = Field(
83
+ default=5.0,
84
+ description="Time in seconds before a keep-alive connection is closed. Keep this low for serverless.",
85
+ )
ritten/decorators.py ADDED
@@ -0,0 +1,45 @@
1
+ from functools import wraps
2
+ import httpx
3
+ from pydantic import ValidationError
4
+ import json
5
+ from ritten.exceptions import (
6
+ RittenError,
7
+ RittenConnectionError,
8
+ RittenValueError,
9
+ RittenParseError,
10
+ )
11
+
12
+
13
+ def exception_handler(func):
14
+ """
15
+ Decorator that translates third-party exceptions into native SDK exceptions.
16
+ """
17
+
18
+ @wraps(func)
19
+ def wrapper(*args, **kwargs):
20
+ try:
21
+ return func(*args, **kwargs)
22
+
23
+ # If it's a RittenError exception, reraise it.
24
+ except RittenError:
25
+ raise
26
+
27
+ # Network-level failures
28
+ except httpx.RequestError as e:
29
+ raise RittenConnectionError(f"A network error occurred: {str(e)}") from e
30
+
31
+ # Validation errors
32
+ except ValidationError as e:
33
+ raise RittenValueError(f"Data validation failed: {str(e)}") from e
34
+
35
+ # JSON parsing errors
36
+ except json.JSONDecodeError as e:
37
+ raise RittenParseError(
38
+ f"Failed to parse API response as JSON: {str(e)}"
39
+ ) from e
40
+
41
+ # The Ultimate Catch-All for anything else
42
+ except Exception as e:
43
+ raise RittenError(f"An unexpected SDK error occurred: {str(e)}") from e
44
+
45
+ return wrapper
ritten/exceptions.py ADDED
@@ -0,0 +1,95 @@
1
+ """
2
+ Ritten SDK Exceptions.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+
9
+ class RittenError(Exception):
10
+ """The base exception for all Ritten SDK errors."""
11
+
12
+ pass
13
+
14
+
15
+ class RittenClientError(RittenError):
16
+ """Client is misconfigured or used incorrectly."""
17
+
18
+ pass
19
+
20
+
21
+ class RittenConnectionError(RittenError):
22
+ """SDK cannot reach the API."""
23
+
24
+ pass
25
+
26
+
27
+ class RittenValueError(RittenError):
28
+ """Invalid value provided to a method."""
29
+
30
+ pass
31
+
32
+
33
+ class RittenParseError(RittenError):
34
+ """Failed to parse API response."""
35
+
36
+ pass
37
+
38
+
39
+ class RittenAuthError(RittenError):
40
+ """Authentication error."""
41
+
42
+ pass
43
+
44
+
45
+ # HTTP Response Errors
46
+ class RittenAPIError(RittenError):
47
+ """Base class for errors returned by the Ritten API (HTTP 4xx and 5xx)."""
48
+
49
+ def __init__(self, message: str, status_code: int, payload: dict | None = None):
50
+ super().__init__(message)
51
+ self.status_code = status_code
52
+ self.payload = payload or {}
53
+
54
+
55
+ class RittenUnauthorizedError(RittenAPIError):
56
+ """401 Unauthorized or 403 Forbidden."""
57
+
58
+ pass
59
+
60
+
61
+ class RittenValidationError(RittenAPIError):
62
+ """400 Bad Request or 422 Unprocessable Entity."""
63
+
64
+ pass
65
+
66
+
67
+ class RittenNotFoundError(RittenAPIError):
68
+ """404 Not Found."""
69
+
70
+ pass
71
+
72
+
73
+ class RittenRateLimitError(RittenAPIError):
74
+ """429 Too Many Requests."""
75
+
76
+ pass
77
+
78
+
79
+ class RittenServerError(RittenAPIError):
80
+ """5xx Server Errors (e.g., 500, 502, 503)."""
81
+
82
+ pass
83
+
84
+
85
+ ERROR_MAP = {
86
+ 400: RittenValidationError,
87
+ 403: RittenUnauthorizedError,
88
+ 404: RittenNotFoundError,
89
+ 422: RittenValidationError,
90
+ 429: RittenRateLimitError,
91
+ 500: RittenServerError,
92
+ 502: RittenServerError,
93
+ 503: RittenServerError,
94
+ }
95
+ """Mapping of HTTP status codes to specific error classes for structured error handling."""
@@ -0,0 +1,32 @@
1
+ """
2
+ Ritten SDK Resources.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ from ritten.resources.resource import Resource
9
+ from ritten.resources.calendar import Calendar
10
+ from ritten.resources.cases import Cases
11
+ from ritten.resources.contacts import Contacts
12
+ from ritten.resources.facilities import Facilities
13
+ from ritten.resources.forms import Forms
14
+ from ritten.resources.insurance import Insurance
15
+ from ritten.resources.organizations import Organizations
16
+ from ritten.resources.patients import Patients
17
+ from ritten.resources.programs import Programs
18
+ from ritten.resources.users import Users
19
+
20
+ __all__ = [
21
+ "Resource",
22
+ "Calendar",
23
+ "Cases",
24
+ "Contacts",
25
+ "Facilities",
26
+ "Forms",
27
+ "Insurance",
28
+ "Organizations",
29
+ "Patients",
30
+ "Programs",
31
+ "Users",
32
+ ]
@@ -0,0 +1,32 @@
1
+ """
2
+ Ritten SDK Calendar Resource.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from typing import Dict, Any
10
+
11
+ from ritten.resources.resource import Resource
12
+
13
+
14
+ class Calendar(Resource):
15
+ """
16
+ Handles all interactions with the Ritten API `/calendar/events` endpoints.
17
+ """
18
+
19
+ def __init__(self, client: httpx.Client):
20
+ self._client = client
21
+ self._base_path = "/calendar/events"
22
+
23
+ def list(self, payload: Dict[str, Any]) -> Dict[str, Any]:
24
+ """
25
+ Queries calendar events based on specific criteria.
26
+ The POST body is the query object.
27
+ """
28
+ return self._client.post(f"{self._base_path}/list", json=payload).json()
29
+
30
+ def create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
31
+ """Creates a new calendar event."""
32
+ return self._client.post(f"{self._base_path}", json=payload).json()
@@ -0,0 +1,51 @@
1
+ """
2
+ Ritten SDK Cases Resource.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from typing import Dict, Any
10
+
11
+ from ritten.resources.resource import Resource
12
+
13
+
14
+ class Cases(Resource):
15
+ """
16
+ Handles all interactions with the Ritten API `/cases` endpoints.
17
+ """
18
+
19
+ def __init__(self, client: httpx.Client):
20
+ self._client = client
21
+ self._base_path = "/cases"
22
+
23
+ def list(self, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
24
+ """Lists cases in a clinic."""
25
+ params = {
26
+ "limit": limit,
27
+ "offset": offset,
28
+ }
29
+ return self._client.get(self._base_path, params=params).json()
30
+
31
+ def get(self, id: str) -> Dict[str, Any]:
32
+ """Retrieves a single case by its ID."""
33
+ return self._client.get(f"{self._base_path}/{id}").json()
34
+
35
+ def create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
36
+ """Creates a new case."""
37
+ return self._client.post(self._base_path, json=payload).json()
38
+
39
+ def update(self, id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
40
+ """Updates an existing case by ID."""
41
+ return self._client.patch(
42
+ f"{self._base_path}/{id}",
43
+ json=payload,
44
+ ).json()
45
+
46
+ def create_note(self, id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
47
+ """Creates a note for a specific case."""
48
+ return self._client.post(
49
+ f"{self._base_path}/{id}/notes",
50
+ json=payload,
51
+ ).json()
@@ -0,0 +1,52 @@
1
+ """
2
+ Ritten SDK Contacts Resource.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from typing import Dict, Any
10
+
11
+ from ritten.resources.resource import Resource
12
+
13
+
14
+ class Contacts(Resource):
15
+ """
16
+ Handles all interactions with the Ritten API `/contacts` endpoints.
17
+ """
18
+
19
+ def __init__(self, client: httpx.Client):
20
+ self._client = client
21
+ self._base_path = "/contacts"
22
+
23
+ def list(self, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
24
+ """Lists contacts in a clinic."""
25
+ params = {"limit": limit, "offset": offset}
26
+ return self._client.get(self._base_path, params=params).json()
27
+
28
+ def get(self, id: str) -> Dict[str, Any]:
29
+ """Retrieves a single contact by their ID."""
30
+ return self._client.get(f"{self._base_path}/{id}").json()
31
+
32
+ def create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
33
+ """Creates a new contact."""
34
+ return self._client.post(self._base_path, json=payload).json()
35
+
36
+ def update(self, id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
37
+ """Updates an existing contact by ID."""
38
+ return self._client.patch(
39
+ f"{self._base_path}/{id}",
40
+ json=payload,
41
+ ).json()
42
+
43
+ def list_relationships(self, id: str) -> Dict[str, Any]:
44
+ """Lists a contact's relationships."""
45
+ return self._client.get(f"{self._base_path}/{id}/relationships").json()
46
+
47
+ def create_relationship(self, id: str, payload: Dict[str, Any]) -> Dict[str, Any]:
48
+ """Creates a new relationship for a contact."""
49
+ return self._client.post(
50
+ f"{self._base_path}/{id}/relationships",
51
+ json=payload,
52
+ ).json()
@@ -0,0 +1,54 @@
1
+ """
2
+ Ritten SDK Facilities Resource.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from typing import Dict, Any
10
+ from datetime import datetime
11
+
12
+ from ritten.resources.resource import Resource
13
+ from ritten.utils import to_iso_format
14
+
15
+
16
+ class Facilities(Resource):
17
+ """
18
+ Handles all interactions with the Ritten API `/facilities` endpoints.
19
+ """
20
+
21
+ def __init__(self, client: httpx.Client):
22
+ self._client = client
23
+ self._base_path = "/facilities"
24
+
25
+ def list(
26
+ self,
27
+ limit: int = 20,
28
+ offset: int = 0,
29
+ search: str | None = None,
30
+ created_after: datetime | None = None,
31
+ ) -> Dict[str, Any]:
32
+ """Lists active clinic facilities.
33
+
34
+ Arguments:
35
+ limit: Number of facilities to return (default: 20).
36
+ offset: Number of facilities to skip for pagination (default: 0).
37
+ search: Optional case-insensitive search to filter facilities by name.
38
+ created_after: Optional datetime to filter facilities created after this date.
39
+ """
40
+ params = {"limit": limit, "offset": offset}
41
+ if search:
42
+ params["search"] = search
43
+ if created_after:
44
+ params["createdAfter"] = to_iso_format(created_after)
45
+ return self._client.get(self._base_path, params=params).json()
46
+
47
+ def create(self, payload: Dict[str, Any]) -> Dict[str, Any]:
48
+ """Creates a new facility."""
49
+ return self._client.post(self._base_path, json=payload).json()
50
+
51
+ def update(self, id: str, name: str) -> None | Dict[str, Any]:
52
+ """Updates an existing facility by ID."""
53
+ payload = {"name": name}
54
+ return self._client.patch(f"{self._base_path}/{id}", json=payload).json()
@@ -0,0 +1,32 @@
1
+ """
2
+ Ritten SDK Forms Resource.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from typing import Dict, Any
10
+
11
+ from ritten.resources.resource import Resource
12
+
13
+
14
+ class Forms(Resource):
15
+ """
16
+ Handles all interactions with the Ritten API `/forms/definitions` endpoints.
17
+ """
18
+
19
+ def __init__(self, client: httpx.Client):
20
+ self._client = client
21
+ self._base_path = "/forms/definitions"
22
+
23
+ def list(
24
+ self, show_archived: bool, is_tx_plan: bool, field_definition_ids: list[str]
25
+ ) -> Dict[str, Any]:
26
+ """List all form definitions configured for the clinic, including section definitions and signature requirements."""
27
+ params = {
28
+ "showArchived": show_archived,
29
+ "isTxPlan": is_tx_plan,
30
+ "fieldDefinitionIds": field_definition_ids,
31
+ }
32
+ return self._client.get(f"{self._base_path}", params=params).json()
@@ -0,0 +1,29 @@
1
+ """
2
+ Ritten SDK Insurance Resource.
3
+
4
+ :copyright: (c) 2026 by Wesley Gonçalves.
5
+ :license: MIT, see LICENSE for more details.
6
+ """
7
+
8
+ import httpx
9
+ from typing import Dict, Any
10
+
11
+ from ritten.resources.resource import Resource
12
+
13
+
14
+ class Insurance(Resource):
15
+ """
16
+ Handles all interactions with the Ritten API `/insurance/payers` endpoints.
17
+ """
18
+
19
+ def __init__(self, client: httpx.Client):
20
+ self._client = client
21
+ self._base_path = "/insurance/payers"
22
+
23
+ def list(self) -> Dict[str, Any]:
24
+ """List all insurance payers."""
25
+ return self._client.get(f"{self._base_path}").json()
26
+
27
+ def get(self, id: str) -> Dict[str, Any]:
28
+ """Retrieves a single insurance payer by their ID."""
29
+ return self._client.get(f"{self._base_path}/{id}").json()