memcell 0.1.1__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.
- memcell/__init__.py +45 -0
- memcell/auth.py +137 -0
- memcell/client.py +883 -0
- memcell/exceptions.py +52 -0
- memcell/models.py +100 -0
- memcell/organization.py +259 -0
- memcell/scoped.py +372 -0
- memcell-0.1.1.dist-info/METADATA +112 -0
- memcell-0.1.1.dist-info/RECORD +10 -0
- memcell-0.1.1.dist-info/WHEEL +4 -0
memcell/__init__.py
ADDED
|
@@ -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
|
+
]
|
memcell/auth.py
ADDED
|
@@ -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
|