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.
- glytos-0.1.0/.gitignore +5 -0
- glytos-0.1.0/LICENSE +21 -0
- glytos-0.1.0/PKG-INFO +102 -0
- glytos-0.1.0/README.md +83 -0
- glytos-0.1.0/glytos/__init__.py +21 -0
- glytos-0.1.0/glytos/_client.py +212 -0
- glytos-0.1.0/glytos/_webhooks.py +51 -0
- glytos-0.1.0/pyproject.toml +31 -0
glytos-0.1.0/.gitignore
ADDED
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"]
|