memcell 0.1.1__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.
- memcell-0.1.1/.gitignore +34 -0
- memcell-0.1.1/CHANGELOG.md +8 -0
- memcell-0.1.1/PKG-INFO +112 -0
- memcell-0.1.1/README.md +86 -0
- memcell-0.1.1/pyproject.toml +65 -0
- memcell-0.1.1/src/memcell/__init__.py +45 -0
- memcell-0.1.1/src/memcell/auth.py +137 -0
- memcell-0.1.1/src/memcell/client.py +883 -0
- memcell-0.1.1/src/memcell/exceptions.py +52 -0
- memcell-0.1.1/src/memcell/models.py +100 -0
- memcell-0.1.1/src/memcell/organization.py +259 -0
- memcell-0.1.1/src/memcell/scoped.py +372 -0
- memcell-0.1.1/tests/test_auth.py +97 -0
- memcell-0.1.1/tests/test_client.py +191 -0
- memcell-0.1.1/tests/test_rate_limit.py +125 -0
memcell-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
|
|
5
|
+
# Build artifacts
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.eggs/
|
|
10
|
+
|
|
11
|
+
# Python
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
*$py.class
|
|
15
|
+
.venv/
|
|
16
|
+
env/
|
|
17
|
+
venv/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# Logs & Environment
|
|
25
|
+
*.log
|
|
26
|
+
.env
|
|
27
|
+
.env.*
|
|
28
|
+
!.env.example
|
|
29
|
+
|
|
30
|
+
# OS & Editors
|
|
31
|
+
.DS_Store
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.swp
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1](https://github.com/memcell-ai/sdk/compare/python-v0.1.0...python-v0.1.1) (2026-09-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* initialize multi-language sdk monorepo for typescript and python ([d5da445](https://github.com/memcell-ai/sdk/commit/d5da4457c5f6f3f87287f41f21acb921592f0d0a))
|
memcell-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: memcell
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Official Python SDK for MemCell persistent cognitive memory and reasoning engine
|
|
5
|
+
Author-email: MemCell Team <engineering@memcell.io>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx>=0.27.0
|
|
19
|
+
Requires-Dist: pydantic>=2.0.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: respx>=0.21.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# memcell
|
|
28
|
+
|
|
29
|
+
Official Python SDK for [MemCell](https://memcell.io)—persistent cognitive memory and reasoning substrate for AI coding agents.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install memcell
|
|
35
|
+
# or
|
|
36
|
+
uv add memcell
|
|
37
|
+
# or
|
|
38
|
+
poetry add memcell
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
### Asynchronous Client (`AsyncMemCell`)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import asyncio
|
|
47
|
+
from memcell import AsyncMemCell
|
|
48
|
+
|
|
49
|
+
async def main():
|
|
50
|
+
async with AsyncMemCell(api_key="mc_live_...") as memory:
|
|
51
|
+
# Pre-flight recall before an agent acts
|
|
52
|
+
recall = await memory.recall(
|
|
53
|
+
namespace="acme/devops",
|
|
54
|
+
query="how to deploy the canary pipeline"
|
|
55
|
+
)
|
|
56
|
+
print(recall.prompt_context)
|
|
57
|
+
|
|
58
|
+
# In-flight scoped execution with automatic reinforcement
|
|
59
|
+
devops = memory.scope("acme/devops", subject="pipeline:canary")
|
|
60
|
+
result = await devops.wrap_execution(
|
|
61
|
+
action="deploy_canary",
|
|
62
|
+
external_ref="ci:run:1049",
|
|
63
|
+
fn=lambda ctx: print("Deploying with context:", ctx.prompt_context)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Synchronous Client (`MemCell`)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from memcell import MemCell
|
|
73
|
+
|
|
74
|
+
memory = MemCell(api_key="mc_live_...")
|
|
75
|
+
|
|
76
|
+
recall = memory.recall(
|
|
77
|
+
namespace="acme/backend",
|
|
78
|
+
query="database connection pool configuration"
|
|
79
|
+
)
|
|
80
|
+
for statement in recall.statements:
|
|
81
|
+
print(f"[{statement.kind}] {statement.title} (confidence: {statement.confidence})")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### OAuth 2.0 Machine-to-Machine (M2M)
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from memcell import AsyncMemCell
|
|
88
|
+
|
|
89
|
+
memory = AsyncMemCell(
|
|
90
|
+
client_id="client_xyz",
|
|
91
|
+
client_secret="secret_xyz",
|
|
92
|
+
scope="memory:read:acme/* memory:write:acme/backend"
|
|
93
|
+
)
|
|
94
|
+
# Automatically handles token exchange and proactively refreshes before expiration!
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Rate Limiting & Resilience
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from memcell import AsyncMemCell
|
|
101
|
+
|
|
102
|
+
memory = AsyncMemCell(
|
|
103
|
+
api_key="mc_live_...",
|
|
104
|
+
max_retries=3,
|
|
105
|
+
on_rate_limit_warning=lambda warning, response: print(f"Warning: {warning}")
|
|
106
|
+
)
|
|
107
|
+
# Automatically performs jittered exponential backoff respecting server Retry-After!
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
Apache-2.0 © [MemCell](https://memcell.io)
|
memcell-0.1.1/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# memcell
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [MemCell](https://memcell.io)—persistent cognitive memory and reasoning substrate for AI coding agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install memcell
|
|
9
|
+
# or
|
|
10
|
+
uv add memcell
|
|
11
|
+
# or
|
|
12
|
+
poetry add memcell
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
### Asynchronous Client (`AsyncMemCell`)
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
from memcell import AsyncMemCell
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
async with AsyncMemCell(api_key="mc_live_...") as memory:
|
|
25
|
+
# Pre-flight recall before an agent acts
|
|
26
|
+
recall = await memory.recall(
|
|
27
|
+
namespace="acme/devops",
|
|
28
|
+
query="how to deploy the canary pipeline"
|
|
29
|
+
)
|
|
30
|
+
print(recall.prompt_context)
|
|
31
|
+
|
|
32
|
+
# In-flight scoped execution with automatic reinforcement
|
|
33
|
+
devops = memory.scope("acme/devops", subject="pipeline:canary")
|
|
34
|
+
result = await devops.wrap_execution(
|
|
35
|
+
action="deploy_canary",
|
|
36
|
+
external_ref="ci:run:1049",
|
|
37
|
+
fn=lambda ctx: print("Deploying with context:", ctx.prompt_context)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
asyncio.run(main())
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Synchronous Client (`MemCell`)
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from memcell import MemCell
|
|
47
|
+
|
|
48
|
+
memory = MemCell(api_key="mc_live_...")
|
|
49
|
+
|
|
50
|
+
recall = memory.recall(
|
|
51
|
+
namespace="acme/backend",
|
|
52
|
+
query="database connection pool configuration"
|
|
53
|
+
)
|
|
54
|
+
for statement in recall.statements:
|
|
55
|
+
print(f"[{statement.kind}] {statement.title} (confidence: {statement.confidence})")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### OAuth 2.0 Machine-to-Machine (M2M)
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from memcell import AsyncMemCell
|
|
62
|
+
|
|
63
|
+
memory = AsyncMemCell(
|
|
64
|
+
client_id="client_xyz",
|
|
65
|
+
client_secret="secret_xyz",
|
|
66
|
+
scope="memory:read:acme/* memory:write:acme/backend"
|
|
67
|
+
)
|
|
68
|
+
# Automatically handles token exchange and proactively refreshes before expiration!
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Rate Limiting & Resilience
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from memcell import AsyncMemCell
|
|
75
|
+
|
|
76
|
+
memory = AsyncMemCell(
|
|
77
|
+
api_key="mc_live_...",
|
|
78
|
+
max_retries=3,
|
|
79
|
+
on_rate_limit_warning=lambda warning, response: print(f"Warning: {warning}")
|
|
80
|
+
)
|
|
81
|
+
# Automatically performs jittered exponential backoff respecting server Retry-After!
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
Apache-2.0 © [MemCell](https://memcell.io)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "memcell"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Official Python SDK for MemCell persistent cognitive memory and reasoning engine"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "MemCell Team", email = "engineering@memcell.io" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: Apache Software License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"httpx>=0.27.0",
|
|
29
|
+
"pydantic>=2.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest>=8.0.0",
|
|
35
|
+
"pytest-asyncio>=0.23.0",
|
|
36
|
+
"respx>=0.21.0",
|
|
37
|
+
"ruff>=0.3.0",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py310"
|
|
43
|
+
include = ["src/**/*.py", "tests/**/*.py"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = [
|
|
47
|
+
"E",
|
|
48
|
+
"F",
|
|
49
|
+
"W",
|
|
50
|
+
"I",
|
|
51
|
+
"UP",
|
|
52
|
+
"RUF",
|
|
53
|
+
]
|
|
54
|
+
ignore = [
|
|
55
|
+
"E501",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
"tests/*" = ["S101"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
asyncio_mode = "auto"
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
pythonpath = ["src"]
|
|
65
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from .auth import AuthManager
|
|
2
|
+
from .client import AsyncMemCell, MemCell
|
|
3
|
+
from .exceptions import MemCellError, RateLimitError
|
|
4
|
+
from .models import (
|
|
5
|
+
FeedbackResponse,
|
|
6
|
+
JobEvent,
|
|
7
|
+
MemoryKind,
|
|
8
|
+
MemoryStatus,
|
|
9
|
+
OrganizationItem,
|
|
10
|
+
OutcomeVerdict,
|
|
11
|
+
RecallResponse,
|
|
12
|
+
RememberResponse,
|
|
13
|
+
ReportResponse,
|
|
14
|
+
ScopedExecutionContext,
|
|
15
|
+
StatementItem,
|
|
16
|
+
)
|
|
17
|
+
from .organization import AsyncOrganizationMemCell, OrganizationMemCell
|
|
18
|
+
from .scoped import AsyncScopedMemCell, ScopedExecutionResult, ScopedMemCell
|
|
19
|
+
|
|
20
|
+
__version__ = "0.1.1"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"AsyncMemCell",
|
|
24
|
+
"AsyncOrganizationMemCell",
|
|
25
|
+
"AsyncScopedMemCell",
|
|
26
|
+
"AuthManager",
|
|
27
|
+
"FeedbackResponse",
|
|
28
|
+
"JobEvent",
|
|
29
|
+
"MemCell",
|
|
30
|
+
"MemCellError",
|
|
31
|
+
"MemoryKind",
|
|
32
|
+
"MemoryStatus",
|
|
33
|
+
"OrganizationItem",
|
|
34
|
+
"OrganizationMemCell",
|
|
35
|
+
"OutcomeVerdict",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"RecallResponse",
|
|
38
|
+
"RememberResponse",
|
|
39
|
+
"ReportResponse",
|
|
40
|
+
"ScopedExecutionContext",
|
|
41
|
+
"ScopedExecutionResult",
|
|
42
|
+
"ScopedMemCell",
|
|
43
|
+
"StatementItem",
|
|
44
|
+
"__version__",
|
|
45
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from .exceptions import MemCellError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthManager:
|
|
10
|
+
"""Manages authentication tokens, API keys, and OAuth 2.0 M2M client credentials."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
base_url: str,
|
|
15
|
+
api_key: str | None = None,
|
|
16
|
+
access_token: str | None = None,
|
|
17
|
+
client_id: str | None = None,
|
|
18
|
+
client_secret: str | None = None,
|
|
19
|
+
scope: str | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.base_url = base_url.rstrip("/")
|
|
22
|
+
self.api_key = api_key
|
|
23
|
+
self.access_token = access_token
|
|
24
|
+
self.client_id = client_id
|
|
25
|
+
self.client_secret = client_secret
|
|
26
|
+
self.scope = scope
|
|
27
|
+
|
|
28
|
+
self._cached_token: str | None = None
|
|
29
|
+
self._token_expires_at: float = 0.0
|
|
30
|
+
|
|
31
|
+
def clear_cache(self) -> None:
|
|
32
|
+
"""Clears cached OAuth M2M access token."""
|
|
33
|
+
self._cached_token = None
|
|
34
|
+
self._token_expires_at = 0.0
|
|
35
|
+
|
|
36
|
+
def get_authorization_header(self, client: httpx.Client | None = None) -> str | None:
|
|
37
|
+
"""Synchronously resolves Authorization header value."""
|
|
38
|
+
if self.api_key:
|
|
39
|
+
return f"Bearer {self.api_key}"
|
|
40
|
+
if self.access_token:
|
|
41
|
+
return f"Bearer {self.access_token}"
|
|
42
|
+
if self.client_id and self.client_secret:
|
|
43
|
+
# Check proactive 60s pre-expiry window
|
|
44
|
+
now = time.time()
|
|
45
|
+
if self._cached_token and (now + 60.0) < self._token_expires_at:
|
|
46
|
+
return f"Bearer {self._cached_token}"
|
|
47
|
+
|
|
48
|
+
c = client or httpx.Client()
|
|
49
|
+
should_close = client is None
|
|
50
|
+
try:
|
|
51
|
+
data = {
|
|
52
|
+
"grant_type": "client_credentials",
|
|
53
|
+
"client_id": self.client_id,
|
|
54
|
+
"client_secret": self.client_secret,
|
|
55
|
+
}
|
|
56
|
+
if self.scope:
|
|
57
|
+
data["scope"] = self.scope
|
|
58
|
+
|
|
59
|
+
resp = c.post(
|
|
60
|
+
f"{self.base_url}/oauth2/token",
|
|
61
|
+
data=data,
|
|
62
|
+
headers={
|
|
63
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
64
|
+
"Accept": "application/json",
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
if not resp.is_success:
|
|
68
|
+
raise MemCellError(
|
|
69
|
+
f"OAuth M2M token exchange failed: HTTP {resp.status_code} {resp.text}",
|
|
70
|
+
status=resp.status_code,
|
|
71
|
+
)
|
|
72
|
+
payload: dict[str, Any] = resp.json()
|
|
73
|
+
token = payload.get("access_token")
|
|
74
|
+
expires_in = payload.get("expires_in", 3600)
|
|
75
|
+
if not token:
|
|
76
|
+
raise MemCellError("OAuth token endpoint returned empty access_token.")
|
|
77
|
+
|
|
78
|
+
self._cached_token = str(token)
|
|
79
|
+
self._token_expires_at = time.time() + float(expires_in)
|
|
80
|
+
return f"Bearer {self._cached_token}"
|
|
81
|
+
finally:
|
|
82
|
+
if should_close:
|
|
83
|
+
c.close()
|
|
84
|
+
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
async def get_authorization_header_async(
|
|
88
|
+
self, client: httpx.AsyncClient | None = None
|
|
89
|
+
) -> str | None:
|
|
90
|
+
"""Asynchronously resolves Authorization header value."""
|
|
91
|
+
if self.api_key:
|
|
92
|
+
return f"Bearer {self.api_key}"
|
|
93
|
+
if self.access_token:
|
|
94
|
+
return f"Bearer {self.access_token}"
|
|
95
|
+
if self.client_id and self.client_secret:
|
|
96
|
+
now = time.time()
|
|
97
|
+
if self._cached_token and (now + 60.0) < self._token_expires_at:
|
|
98
|
+
return f"Bearer {self._cached_token}"
|
|
99
|
+
|
|
100
|
+
c = client or httpx.AsyncClient()
|
|
101
|
+
should_close = client is None
|
|
102
|
+
try:
|
|
103
|
+
data = {
|
|
104
|
+
"grant_type": "client_credentials",
|
|
105
|
+
"client_id": self.client_id,
|
|
106
|
+
"client_secret": self.client_secret,
|
|
107
|
+
}
|
|
108
|
+
if self.scope:
|
|
109
|
+
data["scope"] = self.scope
|
|
110
|
+
|
|
111
|
+
resp = await c.post(
|
|
112
|
+
f"{self.base_url}/oauth2/token",
|
|
113
|
+
data=data,
|
|
114
|
+
headers={
|
|
115
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
116
|
+
"Accept": "application/json",
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
if not resp.is_success:
|
|
120
|
+
raise MemCellError(
|
|
121
|
+
f"OAuth M2M token exchange failed: HTTP {resp.status_code} {resp.text}",
|
|
122
|
+
status=resp.status_code,
|
|
123
|
+
)
|
|
124
|
+
payload: dict[str, Any] = resp.json()
|
|
125
|
+
token = payload.get("access_token")
|
|
126
|
+
expires_in = payload.get("expires_in", 3600)
|
|
127
|
+
if not token:
|
|
128
|
+
raise MemCellError("OAuth token endpoint returned empty access_token.")
|
|
129
|
+
|
|
130
|
+
self._cached_token = str(token)
|
|
131
|
+
self._token_expires_at = time.time() + float(expires_in)
|
|
132
|
+
return f"Bearer {self._cached_token}"
|
|
133
|
+
finally:
|
|
134
|
+
if should_close:
|
|
135
|
+
await c.aclose()
|
|
136
|
+
|
|
137
|
+
return None
|