glytos 0.1.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.
@@ -0,0 +1,5 @@
1
+ dist/
2
+ build/
3
+ *.egg-info/
4
+ __pycache__/
5
+ .venv/
glytos-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Glytos
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
glytos-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.4
2
+ Name: glytos
3
+ Version: 0.1.0
4
+ Summary: Official Glytos server SDK for Python.
5
+ Project-URL: Homepage, https://glytos.com
6
+ Project-URL: Documentation, https://glytos.com/en/docs
7
+ Project-URL: Repository, https://github.com/Glytos/backend
8
+ Author: Glytos
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,ai,api,glytos,sdk,voice
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.9
17
+ Requires-Dist: httpx>=0.24
18
+ Description-Content-Type: text/markdown
19
+
20
+ # glytos
21
+
22
+ The official [Glytos](https://glytos.com) server SDK for Python.
23
+
24
+ Call the Glytos API from your backend with an API key: build and run voice agents,
25
+ start phone calls, mint browser web-call tokens, manage phone numbers, and verify
26
+ webhooks.
27
+
28
+ > Never ship an API key to the browser. For in-browser voice, use the `@glytos/web`
29
+ > package with a short-lived token you mint here.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install glytos
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ from glytos import Glytos
41
+
42
+ glytos = Glytos(api_key="gly_...")
43
+
44
+ # List your agents
45
+ agents = glytos.workflows.list()
46
+
47
+ # Mint a web-call token for the browser
48
+ token = glytos.calls.web_token(workflow_uuid=agents[0]["uuid"])
49
+ print(token["token"], token["ws_url"])
50
+ ```
51
+
52
+ Use it as a context manager to close the HTTP connection cleanly:
53
+
54
+ ```python
55
+ with Glytos(api_key="gly_...") as glytos:
56
+ overview = glytos.request("GET", "/analytics/overview")
57
+ ```
58
+
59
+ ## Resources
60
+
61
+ | Namespace | Methods |
62
+ | --- | --- |
63
+ | `glytos.workflows` | `list`, `retrieve`, `create`, `publish`, `delete`, `templates`, `session`, `session_events` |
64
+ | `glytos.calls` | `create`, `list`, `retrieve`, `web_token`, `control` |
65
+ | `glytos.phone_numbers` | `search`, `list`, `provision`, `assign`, `release` |
66
+ | `glytos.sessions` | `list` |
67
+ | `glytos.webhooks` | `list`, `create`, `delete`, `events`, `verify` |
68
+
69
+ Any endpoint without a dedicated helper is one call away with
70
+ `glytos.request(method, path, json=..., params=...)`.
71
+
72
+ ## Errors
73
+
74
+ Non-2xx responses raise a `GlytosError` with the API error `code`, HTTP `status`,
75
+ and the `request_id`:
76
+
77
+ ```python
78
+ from glytos import GlytosError
79
+
80
+ try:
81
+ glytos.workflows.retrieve("missing")
82
+ except GlytosError as err:
83
+ print(err.status, err.code, err.message)
84
+ ```
85
+
86
+ ## Webhooks
87
+
88
+ Verify a delivery came from Glytos before trusting it. Pass the **raw** request
89
+ body, the `X-Glytos-Signature` header, and your endpoint secret:
90
+
91
+ ```python
92
+ from glytos import verify_webhook
93
+
94
+ # e.g. in a Flask/FastAPI handler
95
+ ok = verify_webhook(raw_body, request.headers["X-Glytos-Signature"], webhook_secret)
96
+ if not ok:
97
+ abort(400)
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
glytos-0.1.0/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # glytos
2
+
3
+ The official [Glytos](https://glytos.com) server SDK for Python.
4
+
5
+ Call the Glytos API from your backend with an API key: build and run voice agents,
6
+ start phone calls, mint browser web-call tokens, manage phone numbers, and verify
7
+ webhooks.
8
+
9
+ > Never ship an API key to the browser. For in-browser voice, use the `@glytos/web`
10
+ > package with a short-lived token you mint here.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install glytos
16
+ ```
17
+
18
+ ## Quickstart
19
+
20
+ ```python
21
+ from glytos import Glytos
22
+
23
+ glytos = Glytos(api_key="gly_...")
24
+
25
+ # List your agents
26
+ agents = glytos.workflows.list()
27
+
28
+ # Mint a web-call token for the browser
29
+ token = glytos.calls.web_token(workflow_uuid=agents[0]["uuid"])
30
+ print(token["token"], token["ws_url"])
31
+ ```
32
+
33
+ Use it as a context manager to close the HTTP connection cleanly:
34
+
35
+ ```python
36
+ with Glytos(api_key="gly_...") as glytos:
37
+ overview = glytos.request("GET", "/analytics/overview")
38
+ ```
39
+
40
+ ## Resources
41
+
42
+ | Namespace | Methods |
43
+ | --- | --- |
44
+ | `glytos.workflows` | `list`, `retrieve`, `create`, `publish`, `delete`, `templates`, `session`, `session_events` |
45
+ | `glytos.calls` | `create`, `list`, `retrieve`, `web_token`, `control` |
46
+ | `glytos.phone_numbers` | `search`, `list`, `provision`, `assign`, `release` |
47
+ | `glytos.sessions` | `list` |
48
+ | `glytos.webhooks` | `list`, `create`, `delete`, `events`, `verify` |
49
+
50
+ Any endpoint without a dedicated helper is one call away with
51
+ `glytos.request(method, path, json=..., params=...)`.
52
+
53
+ ## Errors
54
+
55
+ Non-2xx responses raise a `GlytosError` with the API error `code`, HTTP `status`,
56
+ and the `request_id`:
57
+
58
+ ```python
59
+ from glytos import GlytosError
60
+
61
+ try:
62
+ glytos.workflows.retrieve("missing")
63
+ except GlytosError as err:
64
+ print(err.status, err.code, err.message)
65
+ ```
66
+
67
+ ## Webhooks
68
+
69
+ Verify a delivery came from Glytos before trusting it. Pass the **raw** request
70
+ body, the `X-Glytos-Signature` header, and your endpoint secret:
71
+
72
+ ```python
73
+ from glytos import verify_webhook
74
+
75
+ # e.g. in a Flask/FastAPI handler
76
+ ok = verify_webhook(raw_body, request.headers["X-Glytos-Signature"], webhook_secret)
77
+ if not ok:
78
+ abort(400)
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,21 @@
1
+ """Official Glytos server SDK for Python.
2
+
3
+ Call the Glytos API from your backend with an API key: build and run voice agents,
4
+ start phone calls, mint browser web-call tokens, manage phone numbers, and verify
5
+ webhooks.
6
+
7
+ from glytos import Glytos
8
+
9
+ glytos = Glytos(api_key="gly_...")
10
+ agents = glytos.workflows.list()
11
+ token = glytos.calls.web_token(workflow_uuid=agents[0]["uuid"])
12
+
13
+ Never ship an API key to the browser. For in-browser voice, use the ``@glytos/web``
14
+ package with a short-lived token you mint here via ``calls.web_token(...)``.
15
+ """
16
+
17
+ from ._client import Glytos, GlytosError
18
+ from ._webhooks import verify_webhook
19
+
20
+ __all__ = ["Glytos", "GlytosError", "verify_webhook"]
21
+ __version__ = "0.1.0"
@@ -0,0 +1,212 @@
1
+ """Glytos API client and resource namespaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from ._webhooks import verify_webhook
10
+
11
+ DEFAULT_BASE_URL = "https://api.glytos.com/api/v1"
12
+
13
+ JSON = Any
14
+
15
+
16
+ class GlytosError(Exception):
17
+ """Raised on any non-2xx API response. Carries the API error ``code``."""
18
+
19
+ def __init__(self, status: int, code: str, message: str, request_id: str | None = None):
20
+ super().__init__(message)
21
+ self.status = status
22
+ self.code = code
23
+ self.message = message
24
+ self.request_id = request_id
25
+
26
+
27
+ class Glytos:
28
+ """Glytos API client.
29
+
30
+ ``api_key`` is your organization API key (starts with ``gly_``). Use it as a
31
+ context manager, or call ``close()`` when done.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ api_key: str,
37
+ *,
38
+ base_url: str = DEFAULT_BASE_URL,
39
+ timeout: float = 30.0,
40
+ http_client: httpx.Client | None = None,
41
+ ):
42
+ if not api_key:
43
+ raise ValueError("Glytos: an api_key is required")
44
+ self._base_url = base_url.rstrip("/")
45
+ self._http = http_client or httpx.Client(timeout=timeout)
46
+ self._headers = {"X-API-Key": api_key, "Accept": "application/json"}
47
+
48
+ self.workflows = Workflows(self)
49
+ self.calls = Calls(self)
50
+ self.phone_numbers = PhoneNumbers(self)
51
+ self.sessions = Sessions(self)
52
+ self.webhooks = Webhooks(self)
53
+
54
+ def request(
55
+ self,
56
+ method: str,
57
+ path: str,
58
+ *,
59
+ json: JSON | None = None,
60
+ params: dict[str, Any] | None = None,
61
+ ) -> JSON:
62
+ """Low-level request against any endpoint (path relative to the API base)."""
63
+ clean_params = {k: v for k, v in (params or {}).items() if v is not None}
64
+ response = self._http.request(
65
+ method,
66
+ self._base_url + path,
67
+ headers=self._headers,
68
+ json=json,
69
+ params=clean_params or None,
70
+ )
71
+ request_id = response.headers.get("x-request-id")
72
+ if response.is_success:
73
+ if not response.content:
74
+ return None
75
+ try:
76
+ return response.json()
77
+ except ValueError:
78
+ return response.text
79
+ code, message = "error", response.reason_phrase or "Request failed"
80
+ try:
81
+ error = response.json().get("error") or {}
82
+ code = error.get("code", code)
83
+ message = error.get("message", message)
84
+ except ValueError:
85
+ pass
86
+ raise GlytosError(response.status_code, code, message, request_id)
87
+
88
+ def close(self) -> None:
89
+ self._http.close()
90
+
91
+ def __enter__(self) -> Glytos:
92
+ return self
93
+
94
+ def __exit__(self, *_exc: object) -> None:
95
+ self.close()
96
+
97
+
98
+ class _Resource:
99
+ def __init__(self, client: Glytos):
100
+ self._client = client
101
+
102
+
103
+ class Workflows(_Resource):
104
+ """Agents: prompt agents and visual workflows."""
105
+
106
+ def list(self) -> JSON:
107
+ return self._client.request("GET", "/workflows")
108
+
109
+ def retrieve(self, workflow_uuid: str) -> JSON:
110
+ return self._client.request("GET", f"/workflows/{workflow_uuid}")
111
+
112
+ def create(
113
+ self, *, name: str, mode: str = "prompt", config: dict[str, Any] | None = None
114
+ ) -> JSON:
115
+ body: dict[str, Any] = {"name": name, "mode": mode}
116
+ if config is not None:
117
+ body["config"] = config
118
+ return self._client.request("POST", "/workflows", json=body)
119
+
120
+ def publish(self, workflow_uuid: str) -> JSON:
121
+ return self._client.request("POST", f"/workflows/{workflow_uuid}/publish")
122
+
123
+ def delete(self, workflow_uuid: str) -> JSON:
124
+ return self._client.request("DELETE", f"/workflows/{workflow_uuid}")
125
+
126
+ def templates(self) -> JSON:
127
+ return self._client.request("GET", "/workflows/templates")
128
+
129
+ def session(self, workflow_uuid: str, session_uuid: str) -> JSON:
130
+ return self._client.request(
131
+ "GET", f"/workflows/{workflow_uuid}/sessions/{session_uuid}"
132
+ )
133
+
134
+ def session_events(self, workflow_uuid: str, session_uuid: str) -> JSON:
135
+ return self._client.request(
136
+ "GET", f"/workflows/{workflow_uuid}/sessions/{session_uuid}/events"
137
+ )
138
+
139
+
140
+ class Calls(_Resource):
141
+ def create(self, **body: Any) -> JSON:
142
+ return self._client.request("POST", "/calls", json=body)
143
+
144
+ def list(self, **params: Any) -> JSON:
145
+ return self._client.request("GET", "/calls", params=params)
146
+
147
+ def retrieve(self, call_uuid: str) -> JSON:
148
+ return self._client.request("GET", f"/calls/{call_uuid}")
149
+
150
+ def web_token(
151
+ self, *, workflow_uuid: str | None = None, agent: dict[str, Any] | None = None
152
+ ) -> JSON:
153
+ """Mint a short-lived, workflow-scoped token for an in-browser web call."""
154
+ body: dict[str, Any] = {}
155
+ if workflow_uuid is not None:
156
+ body["workflow_uuid"] = workflow_uuid
157
+ if agent is not None:
158
+ body["agent"] = agent
159
+ return self._client.request("POST", "/calls/web-token", json=body)
160
+
161
+ def control(self, call_uuid: str, **body: Any) -> JSON:
162
+ return self._client.request("POST", f"/calls/{call_uuid}/control", json=body)
163
+
164
+
165
+ class PhoneNumbers(_Resource):
166
+ def search(self, **params: Any) -> JSON:
167
+ return self._client.request("GET", "/telephony/numbers/search", params=params)
168
+
169
+ def list(self) -> JSON:
170
+ return self._client.request("GET", "/telephony/numbers")
171
+
172
+ def provision(self, *, e164: str, **body: Any) -> JSON:
173
+ return self._client.request("POST", "/telephony/numbers", json={"e164": e164, **body})
174
+
175
+ def assign(self, number_uuid: str, **body: Any) -> JSON:
176
+ return self._client.request(
177
+ "POST", f"/telephony/numbers/{number_uuid}/assign", json=body
178
+ )
179
+
180
+ def release(self, number_uuid: str) -> JSON:
181
+ return self._client.request("DELETE", f"/telephony/numbers/{number_uuid}")
182
+
183
+
184
+ class Sessions(_Resource):
185
+ def list(self, **params: Any) -> JSON:
186
+ return self._client.request("GET", "/sessions", params=params)
187
+
188
+
189
+ class Webhooks(_Resource):
190
+ def list(self) -> JSON:
191
+ return self._client.request("GET", "/webhooks/endpoints")
192
+
193
+ def create(self, *, url: str, events: list[str], **body: Any) -> JSON:
194
+ return self._client.request(
195
+ "POST", "/webhooks/endpoints", json={"url": url, "events": events, **body}
196
+ )
197
+
198
+ def delete(self, endpoint_id: int | str) -> JSON:
199
+ return self._client.request("DELETE", f"/webhooks/endpoints/{endpoint_id}")
200
+
201
+ def events(self) -> JSON:
202
+ return self._client.request("GET", "/webhooks/events")
203
+
204
+ @staticmethod
205
+ def verify(
206
+ payload: str | bytes,
207
+ signature_header: str,
208
+ secret: str,
209
+ tolerance_seconds: int = 300,
210
+ ) -> bool:
211
+ """Verify a webhook delivery signature (see :func:`glytos.verify_webhook`)."""
212
+ return verify_webhook(payload, signature_header, secret, tolerance_seconds)
@@ -0,0 +1,51 @@
1
+ """Webhook signature verification.
2
+
3
+ Matches the server scheme: HMAC-SHA256 over ``"{timestamp}.{body}"``, delivered in
4
+ the ``X-Glytos-Signature: t=<ts>,v1=<hex>`` header.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import hmac
11
+ import time
12
+
13
+
14
+ def verify_webhook(
15
+ payload: str | bytes,
16
+ signature_header: str,
17
+ secret: str,
18
+ tolerance_seconds: int = 300,
19
+ ) -> bool:
20
+ """Return True only if ``signature_header`` is a valid signature for ``payload``.
21
+
22
+ Pass the RAW request body (``str`` or ``bytes``), the ``X-Glytos-Signature``
23
+ header value, and your endpoint secret. Constant-time and replay-safe.
24
+ """
25
+ body = payload.encode("utf-8") if isinstance(payload, str) else payload
26
+
27
+ parts: dict[str, str] = {}
28
+ for piece in signature_header.split(","):
29
+ key, sep, value = piece.partition("=")
30
+ if sep:
31
+ parts[key.strip()] = value.strip()
32
+
33
+ timestamp = parts.get("t")
34
+ provided = parts.get("v1")
35
+ if not timestamp or not provided:
36
+ return False
37
+
38
+ expected = hmac.new(
39
+ secret.encode("utf-8"), f"{timestamp}.".encode() + body, hashlib.sha256
40
+ ).hexdigest()
41
+ if not hmac.compare_digest(expected, provided):
42
+ return False
43
+
44
+ if tolerance_seconds > 0:
45
+ try:
46
+ ts = int(timestamp)
47
+ except ValueError:
48
+ return False
49
+ if abs(time.time() - ts) > tolerance_seconds:
50
+ return False
51
+ return True
@@ -0,0 +1,31 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "glytos"
7
+ version = "0.1.0"
8
+ description = "Official Glytos server SDK for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Glytos" }]
13
+ keywords = ["glytos", "voice", "ai", "agent", "sdk", "api"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Developers",
19
+ ]
20
+ dependencies = ["httpx>=0.24"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://glytos.com"
24
+ Documentation = "https://glytos.com/en/docs"
25
+ Repository = "https://github.com/Glytos/backend"
26
+
27
+ [tool.hatch.build.targets.wheel]
28
+ packages = ["glytos"]
29
+
30
+ [tool.hatch.build.targets.sdist]
31
+ include = ["glytos", "README.md", "LICENSE"]