diff --git a/README.md b/README.md index e69de29..f7ed2e5 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,70 @@ +# Lunogram Python SDK + +Python SDK for the Lunogram Client API. + +## Installation + +```bash +pip install lunogram-sdk +``` + +## Usage + +Every authenticated Client API endpoint is scoped to a **project**. You supply +the project UUID **once**, when constructing the client, and the SDK injects it +into every request path automatically. You never pass the project per call. + +```python +from lunogram import Lunogram, random_user + +client = Lunogram( + api_key="your-api-key", + project_id="11111111-2222-3333-4444-555555555555", # your project UUID +) + +# Upsert a user +result = client.user.upsert(random_user()) +print(result[0]) # API response + +# Send a user event +client.user.events.post({ + "identifier": [{"source": "default", "external_id": "user_123"}], + "name": "signed_in", +}) + +# Organizations work the same way +client.organization.upsert({ + "identifier": [{"source": "default", "external_id": "org_123"}], + "name": "Acme Inc.", +}) +``` + +> All SDK actions return a tuple: the API response on index `0`, and a possible +> error on index `1`. + +## Project-scoped URLs + +As of this release, every Client API path includes the project UUID as a path +segment: + +``` +/api/client/projects//... +``` + +| Resource | Path (under `/api/client/projects//`) | +| --- | --- | +| Users | `users` | +| User events | `users/events` | +| User scheduled | `users/scheduled` | +| User devices | `users/devices` | +| User inbox | `users/inbox` (`/count`, `/read`, `/archived`) | +| Organizations | `organizations` | +| Organization users | `organizations/users` | +| Organization events | `organizations/events` | +| Organization scheduled | `organizations/scheduled` | +| Organization inbox | `organizations/inbox` (`/count`, `/read`, `/archived`) | +| Push VAPID | `push/vapid` | +| Auth method sessions | `auth-methods//sessions` | + +Authentication is unchanged — the same API key / token is used. Only the URL +gained the project segment. diff --git a/src/lunogram/app/models/objects.py b/src/lunogram/app/models/objects.py index 608432b..9d85047 100644 --- a/src/lunogram/app/models/objects.py +++ b/src/lunogram/app/models/objects.py @@ -1,34 +1,34 @@ from typing import Literal from ..http import httphandler -from ...utils.reference import client as reference_client, default_client +from ...utils.reference import client as reference_client -from src.lunogram.app.types.event import * -from src.lunogram.app.types.scheduled import * +from ..types.event import * +from ..types.scheduled import * entities = Literal["user", "organization"] # .events and .scheduled objects are defined seperately and later integrated with the corresponding entities class events: - def __init__(self, api_key, entity: entities, reference: reference_client | None = None): + def __init__(self, api_key, reference: reference_client, entity: entities): self.entity = entity - self.reference = reference or default_client + self.reference = reference self.handler = httphandler(api_key) - + def post(self, data: Event): match(self.entity): case 'user': req = self.handler.post(self.reference.user.events, data) case 'organization': req = self.handler.post(self.reference.organization.events, data) - + return req class scheduled: - def __init__(self, api_key, entity: entities, reference: reference_client | None = None): + def __init__(self, api_key, reference: reference_client, entity: entities): self.entity = entity - self.reference = reference or default_client + self.reference = reference self.handler = httphandler(api_key) def post(self, data: UpsertScheduled): @@ -39,7 +39,7 @@ def post(self, data: UpsertScheduled): req = self.handler.post(self.reference.organization.scheduled, data) return req - + def delete(self, data: DeleteScheduled): match(self.entity): case 'user': @@ -47,4 +47,4 @@ def delete(self, data: DeleteScheduled): case 'organization': req = self.handler.delete(self.reference.organization.scheduled, data) - return req \ No newline at end of file + return req diff --git a/src/lunogram/app/models/organization.py b/src/lunogram/app/models/organization.py index 80d34bd..5cecb98 100644 --- a/src/lunogram/app/models/organization.py +++ b/src/lunogram/app/models/organization.py @@ -1,21 +1,22 @@ -from src.lunogram.app.models.objects import events, scheduled -from src.lunogram.app.http import httphandler -from src.lunogram.app.types.organization import * +from .objects import events, scheduled +from ..http import httphandler +from ..types.organization import * -from ...utils.reference import client +from ...utils.reference import client as reference_client class organization: - def __init__(self, api_key): - self.events = events(api_key, entity='organization') - self.scheduled = scheduled(api_key, entity='organization') + def __init__(self, api_key, reference: reference_client): + self.reference = reference + self.events = events(api_key, reference, entity='organization') + self.scheduled = scheduled(api_key, reference, entity='organization') self.handler = httphandler(api_key) def upsert(self, data: UpsertOrganization): - req = self.handler.post(client.organization.base, data) + req = self.handler.post(self.reference.organization.base, data) return req - + def delete(self, data: DeleteOrganization): - req = self.handler.delete(client.organization.base, data) + req = self.handler.delete(self.reference.organization.base, data) - return req \ No newline at end of file + return req diff --git a/src/lunogram/app/models/user.py b/src/lunogram/app/models/user.py index 8d61343..a35d0e2 100644 --- a/src/lunogram/app/models/user.py +++ b/src/lunogram/app/models/user.py @@ -1,21 +1,22 @@ -from objects import events, scheduled +from .objects import events, scheduled from ..http import httphandler -from ...utils.reference import client -from src.lunogram.app.types.user import * +from ...utils.reference import client as reference_client +from ..types.user import * class user: - def __init__(self, api_key): - self.events = events(api_key, entity='user') - self.scheduled = scheduled(api_key, entity='user') + def __init__(self, api_key, reference: reference_client): + self.reference = reference + self.events = events(api_key, reference, entity='user') + self.scheduled = scheduled(api_key, reference, entity='user') self.handler = httphandler(api_key) def upsert(self, data: UpsertUser): - req = self.handler.post(client.user.base, data) + req = self.handler.post(self.reference.user.base, data) return req def delete(self, data: DeleteUser): - req = self.handler.delete(client.user.base, data) + req = self.handler.delete(self.reference.user.base, data) - return req \ No newline at end of file + return req diff --git a/src/lunogram/app/objects.py b/src/lunogram/app/objects.py index eb30761..93f87d0 100644 --- a/src/lunogram/app/objects.py +++ b/src/lunogram/app/objects.py @@ -8,70 +8,74 @@ # .events and .scheduled objects are defined seperately and later integrated with the corresponding entities class events: - def __init__(self, api_key, entity: entities): + def __init__(self, api_key, reference: client, entity: entities): self.entity = entity + self.reference = reference self.handler = httphandler(api_key) - + def post(self, data): match(self.entity): case 'user': - req = self.handler.post(client.user.events, data) + req = self.handler.post(self.reference.user.events, data) case 'organization': - req = self.handler.post(client.organization.events, data) - + req = self.handler.post(self.reference.organization.events, data) + return req class scheduled: - def __init__(self, api_key, entity: entities): + def __init__(self, api_key, reference: client, entity: entities): self.entity = entity + self.reference = reference self.handler = httphandler(api_key) def post(self, data): match(self.entity): case 'user': - req = self.handler.post(client.user.scheduled, data) + req = self.handler.post(self.reference.user.scheduled, data) case 'organization': - req = self.handler.post(client.organization.scheduled, data) + req = self.handler.post(self.reference.organization.scheduled, data) return req - + def delete(self, data): match(self.entity): case 'user': - req = self.handler.delete(client.user.scheduled, data) + req = self.handler.delete(self.reference.user.scheduled, data) case 'organization': - req = self.handler.delete(client.organization.scheduled, data) + req = self.handler.delete(self.reference.organization.scheduled, data) return req - + class user: - def __init__(self, api_key): - self.events = events(api_key, entity='user') - self.scheduled = scheduled(api_key, entity='user') + def __init__(self, api_key, reference: client): + self.reference = reference + self.events = events(api_key, reference, entity='user') + self.scheduled = scheduled(api_key, reference, entity='user') self.handler = httphandler(api_key) def upsert(self, data): - req = self.handler.post(client.user.base, data) + req = self.handler.post(self.reference.user.base, data) return req def delete(self, data): - req = self.handler.delete(client.user.base, data) + req = self.handler.delete(self.reference.user.base, data) return req class organization: - def __init__(self, api_key): - self.events = events(api_key, entity='organization') - self.scheduled = scheduled(api_key, entity='organization') + def __init__(self, api_key, reference: client): + self.reference = reference + self.events = events(api_key, reference, entity='organization') + self.scheduled = scheduled(api_key, reference, entity='organization') self.handler = httphandler(api_key) def upsert(self, data): - req = self.handler.post(client.organization.base, data) + req = self.handler.post(self.reference.organization.base, data) return req - + def delete(self, data): - req = self.handler.delete(client.organization.base, data) + req = self.handler.delete(self.reference.organization.base, data) - return req \ No newline at end of file + return req diff --git a/src/lunogram/client.py b/src/lunogram/client.py index ef7f9ae..f52ce29 100644 --- a/src/lunogram/client.py +++ b/src/lunogram/client.py @@ -1,12 +1,17 @@ from random import randint from .utils import seed +from .utils.reference import client as Reference from .app import user, organization class Lunogram: - def __init__(self, api_key): - self.user = user(api_key) - self.organization = organization(api_key) + def __init__(self, api_key, project_id): + # Every Client API endpoint is scoped to a project. The project UUID is + # supplied once here and injected into every request path automatically; + # callers never pass it per request. + self.reference = Reference(project_id) + self.user = user(api_key, self.reference) + self.organization = organization(api_key, self.reference) # Seeder data to generate a random user should you need it, this is mainly for testing purposes def random_user(): @@ -41,8 +46,11 @@ def random_user(): def main() -> None: """ Example use -> - - client = Lunogram("Your API key here") + + client = Lunogram("Your API key here", "your-project-uuid-here") + + > The project UUID is required and is injected into every Client API path, + > e.g. /api/client/projects//users > All sdk actions return a list with the api response on index 0, possible errors on index 1 diff --git a/src/lunogram/utils/reference.py b/src/lunogram/utils/reference.py index 060cd1b..d122c32 100644 --- a/src/lunogram/utils/reference.py +++ b/src/lunogram/utils/reference.py @@ -1,20 +1,69 @@ +from uuid import UUID + url = "https://console.lunogram.com/" api = "/api/client/" destination = url + api +# Every authenticated Client API endpoint is scoped to a single project. The +# project UUID is supplied once when the Lunogram client is constructed and is +# injected here as a `/projects//` path segment, so callers never +# pass it per request. + + +def _validate_project_id(project_id: str) -> str: + if not isinstance(project_id, str) or not project_id.strip(): + raise ValueError("project_id is required and must be a non-empty string") + + try: + UUID(project_id) + except (ValueError, AttributeError, TypeError): + raise ValueError(f"project_id must be a valid UUID, got: {project_id!r}") + + return project_id + + # API reference class _user: - base = destination + 'users' - events = destination + 'users/events' - scheduled = destination + 'users/scheduled' + def __init__(self, project: str): + self.base = project + 'users' + self.events = project + 'users/events' + self.scheduled = project + 'users/scheduled' + self.devices = project + 'users/devices' + self.inbox = project + 'users/inbox' + self.inbox_count = project + 'users/inbox/count' + self.inbox_read = project + 'users/inbox/read' + self.inbox_archived = project + 'users/inbox/archived' class _org: - base = destination + 'organizations' - events = destination + 'organizations/events' - users = destination + 'organizations/users' - scheduled = destination + 'organizations/scheduled' + def __init__(self, project: str): + self.base = project + 'organizations' + self.events = project + 'organizations/events' + self.users = project + 'organizations/users' + self.scheduled = project + 'organizations/scheduled' + self.inbox = project + 'organizations/inbox' + self.inbox_count = project + 'organizations/inbox/count' + self.inbox_read = project + 'organizations/inbox/read' + self.inbox_archived = project + 'organizations/inbox/archived' + +class _push: + def __init__(self, project: str): + self.vapid = project + 'push/vapid' + +class _auth_methods: + def __init__(self, project: str): + self._project = project + + def sessions(self, auth_method_id: str) -> str: + return self._project + f'auth-methods/{auth_method_id}/sessions' class client: - user = _user() - organization = _org() + def __init__(self, project_id: str): + self.project_id = _validate_project_id(project_id) + # e.g. https://console.lunogram.com/api/client/projects// + project = destination + f'projects/{self.project_id}/' + + self.user = _user(project) + self.organization = _org(project) + self.push = _push(project) + self.auth_methods = _auth_methods(project)