memoraeu 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.
@@ -0,0 +1,21 @@
1
+ # Environment
2
+ env/
3
+ .env
4
+
5
+ # MCP config (contains secrets — use .mcp.json.example as template)
6
+ .mcp.json
7
+
8
+ # Database
9
+ memoraeu.db
10
+
11
+ # Python
12
+ __pycache__/
13
+ *.pyc
14
+ *.pyo
15
+ .DS_Store
16
+
17
+ # Node
18
+ dashboard/node_modules/
19
+ landing/node_modules/
20
+ dashboard/dist/
21
+ landing/dist/
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: memoraeu
3
+ Version: 0.1.1
4
+ Summary: Couche mémoire persistante zero-knowledge pour LLMs — client Python
5
+ Project-URL: Homepage, https://memoraeu.com
6
+ Project-URL: Repository, https://github.com/pquattro/memoraeu
7
+ Project-URL: Documentation, https://docs.memoraeu.com
8
+ License: MIT
9
+ Requires-Python: >=3.11
10
+ Requires-Dist: cryptography>=42.0
11
+ Requires-Dist: httpx>=0.27
12
+ Description-Content-Type: text/markdown
13
+
14
+ # MemoraEU SDK — Python
15
+
16
+ Couche mémoire persistante **zero-knowledge** pour LLMs.
17
+ Compatible avec n'importe quel projet Python — sync ou async.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install memoraeu
23
+ ```
24
+
25
+ ## Démarrage rapide
26
+
27
+ ### 1. Obtenir vos clés
28
+
29
+ 1. Créez un compte sur [app.memoraeu.com](https://app.memoraeu.com)
30
+ 2. **Paramètres → Clés de chiffrement** → copiez `MEMORAEU_SALT`
31
+ 3. **Paramètres → Clés API** → créez une clé → copiez `MEMORAEU_API_KEY` (`meu-sk-…`)
32
+ 4. `secret` = votre mot de passe de connexion MemoraEU
33
+
34
+ ### 2. Client sync
35
+
36
+ ```python
37
+ from memoraeu import MemoraEU
38
+
39
+ client = MemoraEU(
40
+ api_url="https://api.memoraeu.com",
41
+ api_key="meu-sk-...", # clé API (authentification HTTP)
42
+ secret="votre-mot-de-passe", # mot de passe MemoraEU (dérivation de clé locale)
43
+ salt="votre-kdf-salt", # salt unique à votre compte (depuis les Paramètres)
44
+ )
45
+
46
+ # Mémoriser
47
+ memory = client.remember(
48
+ "Philippe préfère Python et FastAPI",
49
+ category="prefs",
50
+ tags=["python", "stack"]
51
+ )
52
+
53
+ # Rechercher
54
+ results = client.recall("préférences tech", limit=3)
55
+ for r in results:
56
+ print(f"[{round(r.score*100)}%] {r.memory.content}")
57
+
58
+ # Supprimer
59
+ client.forget(memory.id)
60
+ ```
61
+
62
+ ### 3. Client async
63
+
64
+ ```python
65
+ from memoraeu import AsyncMemoraEU
66
+
67
+ async with AsyncMemoraEU(
68
+ api_url="https://api.memoraeu.com",
69
+ api_key="meu-sk-...",
70
+ secret="votre-mot-de-passe",
71
+ salt="votre-kdf-salt",
72
+ ) as client:
73
+ memory = await client.remember("...")
74
+ results = await client.recall("...")
75
+ ```
76
+
77
+ ## Comprendre les paramètres
78
+
79
+ | Paramètre | Rôle |
80
+ |---|---|
81
+ | `api_key` | Authentification HTTP — Bearer token envoyé à chaque requête (`meu-sk-…`) |
82
+ | `secret` | Votre mot de passe MemoraEU — utilisé comme entrée PBKDF2 pour dériver la clé de chiffrement localement |
83
+ | `salt` | Salt KDF unique à votre compte, généré par le serveur — combiné avec `secret` pour produire la clé AES |
84
+
85
+ > **Zero-knowledge** : la clé de chiffrement est dérivée sur votre machine avec `PBKDF2(secret, salt, 210 000 itérations)`. Le serveur reçoit uniquement le contenu chiffré — il ne peut pas le lire même s'il est compromis.
86
+ >
87
+ > ⚠️ Si vous perdez votre `secret` ou votre `salt`, vos mémoires chiffrées sont irrécupérables.
88
+
89
+ ## Référence
90
+
91
+ | Méthode | Description |
92
+ |---------|-------------|
93
+ | `remember(content, category?, tags?, scope?)` | Mémorise une information |
94
+ | `recall(query, limit=3, category?, scope?)` | Recherche sémantique |
95
+ | `forget(memory_id)` | Supprime une mémoire |
96
+ | `list_memories(category?, limit=20, scope?)` | Liste les mémoires |
97
+ | `list_categories()` | Catégories triées par usage |
98
+
99
+ ## Gestion des erreurs
100
+
101
+ ```python
102
+ from memoraeu import APIError, CryptoError, ConnectionError
103
+
104
+ try:
105
+ client.remember("...")
106
+ except APIError as e:
107
+ print(e.status_code, str(e)) # ex: 401, 409
108
+ except CryptoError as e:
109
+ print("Problème de chiffrement", e)
110
+ except ConnectionError as e:
111
+ print("API inaccessible", e)
112
+ ```
113
+
114
+ ## Auto-hébergement
115
+
116
+ ```python
117
+ client = MemoraEU(
118
+ api_url="http://localhost:8000",
119
+ api_key="meu-sk-...",
120
+ secret="...",
121
+ salt="...",
122
+ )
123
+ ```
124
+
125
+ ## Licence
126
+
127
+ MIT © MemoraEU
@@ -0,0 +1,114 @@
1
+ # MemoraEU SDK — Python
2
+
3
+ Couche mémoire persistante **zero-knowledge** pour LLMs.
4
+ Compatible avec n'importe quel projet Python — sync ou async.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install memoraeu
10
+ ```
11
+
12
+ ## Démarrage rapide
13
+
14
+ ### 1. Obtenir vos clés
15
+
16
+ 1. Créez un compte sur [app.memoraeu.com](https://app.memoraeu.com)
17
+ 2. **Paramètres → Clés de chiffrement** → copiez `MEMORAEU_SALT`
18
+ 3. **Paramètres → Clés API** → créez une clé → copiez `MEMORAEU_API_KEY` (`meu-sk-…`)
19
+ 4. `secret` = votre mot de passe de connexion MemoraEU
20
+
21
+ ### 2. Client sync
22
+
23
+ ```python
24
+ from memoraeu import MemoraEU
25
+
26
+ client = MemoraEU(
27
+ api_url="https://api.memoraeu.com",
28
+ api_key="meu-sk-...", # clé API (authentification HTTP)
29
+ secret="votre-mot-de-passe", # mot de passe MemoraEU (dérivation de clé locale)
30
+ salt="votre-kdf-salt", # salt unique à votre compte (depuis les Paramètres)
31
+ )
32
+
33
+ # Mémoriser
34
+ memory = client.remember(
35
+ "Philippe préfère Python et FastAPI",
36
+ category="prefs",
37
+ tags=["python", "stack"]
38
+ )
39
+
40
+ # Rechercher
41
+ results = client.recall("préférences tech", limit=3)
42
+ for r in results:
43
+ print(f"[{round(r.score*100)}%] {r.memory.content}")
44
+
45
+ # Supprimer
46
+ client.forget(memory.id)
47
+ ```
48
+
49
+ ### 3. Client async
50
+
51
+ ```python
52
+ from memoraeu import AsyncMemoraEU
53
+
54
+ async with AsyncMemoraEU(
55
+ api_url="https://api.memoraeu.com",
56
+ api_key="meu-sk-...",
57
+ secret="votre-mot-de-passe",
58
+ salt="votre-kdf-salt",
59
+ ) as client:
60
+ memory = await client.remember("...")
61
+ results = await client.recall("...")
62
+ ```
63
+
64
+ ## Comprendre les paramètres
65
+
66
+ | Paramètre | Rôle |
67
+ |---|---|
68
+ | `api_key` | Authentification HTTP — Bearer token envoyé à chaque requête (`meu-sk-…`) |
69
+ | `secret` | Votre mot de passe MemoraEU — utilisé comme entrée PBKDF2 pour dériver la clé de chiffrement localement |
70
+ | `salt` | Salt KDF unique à votre compte, généré par le serveur — combiné avec `secret` pour produire la clé AES |
71
+
72
+ > **Zero-knowledge** : la clé de chiffrement est dérivée sur votre machine avec `PBKDF2(secret, salt, 210 000 itérations)`. Le serveur reçoit uniquement le contenu chiffré — il ne peut pas le lire même s'il est compromis.
73
+ >
74
+ > ⚠️ Si vous perdez votre `secret` ou votre `salt`, vos mémoires chiffrées sont irrécupérables.
75
+
76
+ ## Référence
77
+
78
+ | Méthode | Description |
79
+ |---------|-------------|
80
+ | `remember(content, category?, tags?, scope?)` | Mémorise une information |
81
+ | `recall(query, limit=3, category?, scope?)` | Recherche sémantique |
82
+ | `forget(memory_id)` | Supprime une mémoire |
83
+ | `list_memories(category?, limit=20, scope?)` | Liste les mémoires |
84
+ | `list_categories()` | Catégories triées par usage |
85
+
86
+ ## Gestion des erreurs
87
+
88
+ ```python
89
+ from memoraeu import APIError, CryptoError, ConnectionError
90
+
91
+ try:
92
+ client.remember("...")
93
+ except APIError as e:
94
+ print(e.status_code, str(e)) # ex: 401, 409
95
+ except CryptoError as e:
96
+ print("Problème de chiffrement", e)
97
+ except ConnectionError as e:
98
+ print("API inaccessible", e)
99
+ ```
100
+
101
+ ## Auto-hébergement
102
+
103
+ ```python
104
+ client = MemoraEU(
105
+ api_url="http://localhost:8000",
106
+ api_key="meu-sk-...",
107
+ secret="...",
108
+ salt="...",
109
+ )
110
+ ```
111
+
112
+ ## Licence
113
+
114
+ MIT © MemoraEU
@@ -0,0 +1,20 @@
1
+ """
2
+ MemoraEU SDK — Couche mémoire persistante zero-knowledge pour LLMs
3
+ """
4
+ from .client import AsyncMemoraEU, MemoraEU
5
+ from .exceptions import APIError, ConnectionError, CryptoError, MemoraEUError
6
+ from .models import Category, Memory, SearchResult
7
+
8
+ __version__ = "0.1.0"
9
+
10
+ __all__ = [
11
+ "MemoraEU",
12
+ "AsyncMemoraEU",
13
+ "Memory",
14
+ "SearchResult",
15
+ "Category",
16
+ "MemoraEUError",
17
+ "APIError",
18
+ "CryptoError",
19
+ "ConnectionError",
20
+ ]
@@ -0,0 +1,273 @@
1
+ """
2
+ MemoraEU SDK — Client sync et async
3
+
4
+ Usage sync :
5
+ from memoraeu import MemoraEU
6
+ client = MemoraEU(api_url="http://localhost:8000", secret="...", salt="...")
7
+ memory = client.remember("Philippe préfère Python", category="prefs")
8
+ results = client.recall("préférences stack")
9
+
10
+ Usage async :
11
+ from memoraeu import AsyncMemoraEU
12
+ async with AsyncMemoraEU(api_url="...", secret="...", salt="...") as client:
13
+ memory = await client.remember("...")
14
+ results = await client.recall("...")
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import Optional
19
+
20
+ import httpx
21
+
22
+ from .crypto import derive_key, encrypt, decrypt, is_encrypted
23
+ from .exceptions import APIError, CryptoError
24
+ from .exceptions import ConnectionError as MemoraEUConnectionError
25
+ from .models import Category, Memory, SearchResult
26
+
27
+
28
+ class _BaseClient:
29
+ """Logique commune : crypto, sérialisation."""
30
+
31
+ def __init__(
32
+ self,
33
+ api_url: str = "http://localhost:8000",
34
+ api_key: str = "",
35
+ secret: str = "",
36
+ salt: str = "memoraeu-default-salt-v1",
37
+ timeout: float = 90.0,
38
+ ):
39
+ self.api_url = api_url.rstrip("/")
40
+ self.timeout = timeout
41
+ self._headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
42
+ self._key: Optional[bytes] = None
43
+ if secret:
44
+ try:
45
+ self._key = derive_key(secret, salt)
46
+ except Exception as e:
47
+ raise CryptoError(f"Dérivation de clé échouée : {e}") from e
48
+
49
+ # ── Crypto ────────────────────────────────────────────────────────────
50
+
51
+ def _encrypt(self, content: str) -> str:
52
+ if not self._key:
53
+ return content
54
+ return encrypt(content, self._key)
55
+
56
+ def _decrypt(self, value: str) -> str:
57
+ if not self._key:
58
+ return value
59
+ if not is_encrypted(value):
60
+ return value
61
+ try:
62
+ return decrypt(value, self._key)
63
+ except Exception as e:
64
+ raise CryptoError(f"Déchiffrement échoué : {e}") from e
65
+
66
+ # ── Réponses ──────────────────────────────────────────────────────────
67
+
68
+ def _handle_response(self, response: httpx.Response) -> dict:
69
+ try:
70
+ response.raise_for_status()
71
+ except httpx.HTTPStatusError as e:
72
+ raise APIError(response.status_code, response.text) from e
73
+ return response.json()
74
+
75
+ def _parse_memory(self, data: dict) -> Memory:
76
+ return Memory.from_dict(data, decrypt_fn=self._decrypt)
77
+
78
+ def _parse_search_results(self, data: dict) -> list[SearchResult]:
79
+ return [SearchResult.from_dict(r, decrypt_fn=self._decrypt) for r in data.get("results", [])]
80
+
81
+
82
+ # ══════════════════════════════════════════════════════════════════════════
83
+ # Client synchrone
84
+ # ══════════════════════════════════════════════════════════════════════════
85
+
86
+ class MemoraEU(_BaseClient):
87
+ """Client synchrone MemoraEU."""
88
+
89
+ def __init__(self, **kwargs):
90
+ super().__init__(**kwargs)
91
+ self._http = httpx.Client(timeout=self.timeout)
92
+
93
+ def close(self):
94
+ self._http.close()
95
+
96
+ def __enter__(self):
97
+ return self
98
+
99
+ def __exit__(self, *args):
100
+ self.close()
101
+
102
+ def _post(self, path: str, body: dict) -> dict:
103
+ try:
104
+ r = self._http.post(f"{self.api_url}{path}", json=body, headers=self._headers)
105
+ except httpx.ConnectError as e:
106
+ raise MemoraEUConnectionError(f"Impossible de joindre {self.api_url}") from e
107
+ return self._handle_response(r)
108
+
109
+ def _get(self, path: str, params: dict = None) -> dict | list:
110
+ try:
111
+ r = self._http.get(f"{self.api_url}{path}", params=params, headers=self._headers)
112
+ except httpx.ConnectError as e:
113
+ raise MemoraEUConnectionError(f"Impossible de joindre {self.api_url}") from e
114
+ return self._handle_response(r)
115
+
116
+ def _delete(self, path: str) -> bool:
117
+ try:
118
+ r = self._http.delete(f"{self.api_url}{path}", headers=self._headers)
119
+ except httpx.ConnectError as e:
120
+ raise MemoraEUConnectionError(f"Impossible de joindre {self.api_url}") from e
121
+ return r.status_code == 204
122
+
123
+ # ── API publique ──────────────────────────────────────────────────────
124
+
125
+ def remember(
126
+ self,
127
+ content: str,
128
+ category: Optional[str] = None,
129
+ tags: list[str] = None,
130
+ source: str = "sdk",
131
+ scope: str = "private",
132
+ ) -> Memory:
133
+ """Mémorise une information. Le contenu est chiffré côté client si secret fourni."""
134
+ data = self._post("/memories", {
135
+ "content": self._encrypt(content),
136
+ "category": category,
137
+ "tags": tags or [],
138
+ "source": source,
139
+ "scope": scope,
140
+ })
141
+ return self._parse_memory(data)
142
+
143
+ def recall(
144
+ self,
145
+ query: str,
146
+ limit: int = 3,
147
+ category: Optional[str] = None,
148
+ scope: str = "private",
149
+ ) -> list[SearchResult]:
150
+ """Recherche sémantique dans les mémoires."""
151
+ data = self._post("/memories/search", {
152
+ "query": query,
153
+ "limit": limit,
154
+ "category": category,
155
+ "scope": scope,
156
+ })
157
+ return self._parse_search_results(data)
158
+
159
+ def forget(self, memory_id: str) -> bool:
160
+ """Supprime une mémoire par son ID."""
161
+ return self._delete(f"/memories/{memory_id}")
162
+
163
+ def list_memories(
164
+ self,
165
+ category: Optional[str] = None,
166
+ limit: int = 20,
167
+ scope: str = "private",
168
+ ) -> list[Memory]:
169
+ """Liste les mémoires récentes."""
170
+ params = {"limit": limit, "scope": scope}
171
+ if category:
172
+ params["category"] = category
173
+ data = self._get("/memories", params=params)
174
+ return [self._parse_memory(m) for m in data]
175
+
176
+ def list_categories(self) -> list[Category]:
177
+ """Retourne les catégories triées par usage."""
178
+ data = self._get("/memories/categories")
179
+ return [Category(name=c["name"], usage_count=c["usage_count"]) for c in data.get("categories", [])]
180
+
181
+
182
+ # ══════════════════════════════════════════════════════════════════════════
183
+ # Client asynchrone
184
+ # ══════════════════════════════════════════════════════════════════════════
185
+
186
+ class AsyncMemoraEU(_BaseClient):
187
+ """Client asynchrone MemoraEU."""
188
+
189
+ def __init__(self, **kwargs):
190
+ super().__init__(**kwargs)
191
+ self._http: Optional[httpx.AsyncClient] = None
192
+
193
+ async def __aenter__(self):
194
+ self._http = httpx.AsyncClient(timeout=self.timeout)
195
+ return self
196
+
197
+ async def __aexit__(self, *args):
198
+ if self._http:
199
+ await self._http.aclose()
200
+
201
+ async def _post(self, path: str, body: dict) -> dict:
202
+ try:
203
+ r = await self._http.post(f"{self.api_url}{path}", json=body, headers=self._headers)
204
+ except httpx.ConnectError as e:
205
+ raise MemoraEUConnectionError(f"Impossible de joindre {self.api_url}") from e
206
+ return self._handle_response(r)
207
+
208
+ async def _get(self, path: str, params: dict = None) -> dict | list:
209
+ try:
210
+ r = await self._http.get(f"{self.api_url}{path}", params=params, headers=self._headers)
211
+ except httpx.ConnectError as e:
212
+ raise MemoraEUConnectionError(f"Impossible de joindre {self.api_url}") from e
213
+ return self._handle_response(r)
214
+
215
+ async def _delete(self, path: str) -> bool:
216
+ try:
217
+ r = await self._http.delete(f"{self.api_url}{path}", headers=self._headers)
218
+ except httpx.ConnectError as e:
219
+ raise MemoraEUConnectionError(f"Impossible de joindre {self.api_url}") from e
220
+ return r.status_code == 204
221
+
222
+ # ── API publique ──────────────────────────────────────────────────────
223
+
224
+ async def remember(
225
+ self,
226
+ content: str,
227
+ category: Optional[str] = None,
228
+ tags: list[str] = None,
229
+ source: str = "sdk",
230
+ scope: str = "private",
231
+ ) -> Memory:
232
+ data = await self._post("/memories", {
233
+ "content": self._encrypt(content),
234
+ "category": category,
235
+ "tags": tags or [],
236
+ "source": source,
237
+ "scope": scope,
238
+ })
239
+ return self._parse_memory(data)
240
+
241
+ async def recall(
242
+ self,
243
+ query: str,
244
+ limit: int = 3,
245
+ category: Optional[str] = None,
246
+ scope: str = "private",
247
+ ) -> list[SearchResult]:
248
+ data = await self._post("/memories/search", {
249
+ "query": query,
250
+ "limit": limit,
251
+ "category": category,
252
+ "scope": scope,
253
+ })
254
+ return self._parse_search_results(data)
255
+
256
+ async def forget(self, memory_id: str) -> bool:
257
+ return await self._delete(f"/memories/{memory_id}")
258
+
259
+ async def list_memories(
260
+ self,
261
+ category: Optional[str] = None,
262
+ limit: int = 20,
263
+ scope: str = "private",
264
+ ) -> list[Memory]:
265
+ params = {"limit": limit, "scope": scope}
266
+ if category:
267
+ params["category"] = category
268
+ data = await self._get("/memories", params=params)
269
+ return [self._parse_memory(m) for m in data]
270
+
271
+ async def list_categories(self) -> list[Category]:
272
+ data = await self._get("/memories/categories")
273
+ return [Category(name=c["name"], usage_count=c["usage_count"]) for c in data.get("categories", [])]
@@ -0,0 +1,68 @@
1
+ """
2
+ MemoraEU — Chiffrement zero-knowledge côté client
3
+ AES-256-GCM + PBKDF2-HMAC-SHA256
4
+
5
+ Le serveur ne voit jamais le texte clair ni la clé.
6
+ """
7
+ import os
8
+ import base64
9
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
10
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
11
+ from cryptography.hazmat.primitives import hashes
12
+
13
+
14
+ # 210 000 itérations (recommandation OWASP 2024 pour PBKDF2-SHA256)
15
+ PBKDF2_ITERATIONS = 210_000
16
+
17
+ # Préfixe pour détecter les valeurs chiffrées sans ambiguïté
18
+ ENC_PREFIX = "ENCv1:"
19
+
20
+
21
+ def derive_key(password: str, salt: str) -> bytes:
22
+ """Dérive une clé AES-256 depuis un mot de passe et un salt."""
23
+ salt_bytes = salt.encode() if isinstance(salt, str) else salt
24
+ kdf = PBKDF2HMAC(
25
+ algorithm=hashes.SHA256(),
26
+ length=32,
27
+ salt=salt_bytes,
28
+ iterations=PBKDF2_ITERATIONS,
29
+ )
30
+ return kdf.derive(password.encode())
31
+
32
+
33
+ def encrypt(plaintext: str, key: bytes) -> str:
34
+ """
35
+ Chiffre un texte avec AES-256-GCM.
36
+ Retourne : ENCv1:<base64(nonce[12] + ciphertext + tag[16])>
37
+ """
38
+ nonce = os.urandom(12)
39
+ ciphertext = AESGCM(key).encrypt(nonce, plaintext.encode("utf-8"), None)
40
+ return ENC_PREFIX + base64.b64encode(nonce + ciphertext).decode("ascii")
41
+
42
+
43
+ def decrypt(ciphertext_b64: str, key: bytes) -> str:
44
+ """
45
+ Déchiffre un texte chiffré par encrypt().
46
+ Accepte les anciens formats (sans préfixe ENCv1:) pour rétrocompatibilité.
47
+ Lève une exception si la clé est mauvaise ou les données corrompues.
48
+ """
49
+ raw = ciphertext_b64[len(ENC_PREFIX):] if ciphertext_b64.startswith(ENC_PREFIX) else ciphertext_b64
50
+ data = base64.b64decode(raw)
51
+ nonce = data[:12]
52
+ ciphertext = data[12:]
53
+ plaintext = AESGCM(key).decrypt(nonce, ciphertext, None)
54
+ return plaintext.decode("utf-8")
55
+
56
+
57
+ def is_encrypted(value: str) -> bool:
58
+ """
59
+ Détecte si une valeur est chiffrée via le préfixe ENCv1:.
60
+ Rétrocompatible : accepte aussi les anciens blobs base64 bruts (≥ 28 octets).
61
+ """
62
+ if value.startswith(ENC_PREFIX):
63
+ return True
64
+ try:
65
+ data = base64.b64decode(value)
66
+ return len(data) >= 28 # nonce(12) + tag(16) minimum
67
+ except Exception:
68
+ return False
@@ -0,0 +1,23 @@
1
+ """
2
+ MemoraEU SDK — Exceptions
3
+ """
4
+
5
+
6
+ class MemoraEUError(Exception):
7
+ """Erreur générique MemoraEU."""
8
+
9
+
10
+ class APIError(MemoraEUError):
11
+ """Erreur retournée par l'API (HTTP 4xx/5xx)."""
12
+
13
+ def __init__(self, status_code: int, message: str):
14
+ self.status_code = status_code
15
+ super().__init__(f"[{status_code}] {message}")
16
+
17
+
18
+ class CryptoError(MemoraEUError):
19
+ """Erreur de chiffrement/déchiffrement."""
20
+
21
+
22
+ class ConnectionError(MemoraEUError):
23
+ """Impossible de joindre l'API MemoraEU."""
@@ -0,0 +1,57 @@
1
+ """
2
+ MemoraEU SDK — Modèles de données
3
+ """
4
+ from dataclasses import dataclass, field
5
+ from datetime import datetime
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass
10
+ class Memory:
11
+ id: str
12
+ content: str
13
+ category: Optional[str]
14
+ tags: list[str]
15
+ source: str
16
+ scope: str
17
+ org_id: str
18
+ user_id: str
19
+ created_at: datetime
20
+ updated_at: datetime
21
+
22
+ @classmethod
23
+ def from_dict(cls, data: dict, decrypt_fn=None) -> "Memory":
24
+ content = data["content"]
25
+ if decrypt_fn:
26
+ content = decrypt_fn(content)
27
+ return cls(
28
+ id=data["id"],
29
+ content=content,
30
+ category=data.get("category"),
31
+ tags=data.get("tags", []),
32
+ source=data.get("source", "sdk"),
33
+ scope=data.get("scope", "private"),
34
+ org_id=data.get("org_id", ""),
35
+ user_id=data.get("user_id", ""),
36
+ created_at=datetime.fromisoformat(data["created_at"]),
37
+ updated_at=datetime.fromisoformat(data["updated_at"]),
38
+ )
39
+
40
+
41
+ @dataclass
42
+ class SearchResult:
43
+ memory: Memory
44
+ score: float
45
+
46
+ @classmethod
47
+ def from_dict(cls, data: dict, decrypt_fn=None) -> "SearchResult":
48
+ return cls(
49
+ memory=Memory.from_dict(data["memory"], decrypt_fn=decrypt_fn),
50
+ score=data["score"],
51
+ )
52
+
53
+
54
+ @dataclass
55
+ class Category:
56
+ name: str
57
+ usage_count: int
@@ -0,0 +1,20 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "memoraeu"
7
+ version = "0.1.1"
8
+ description = "Couche mémoire persistante zero-knowledge pour LLMs — client Python"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ dependencies = [
13
+ "httpx>=0.27",
14
+ "cryptography>=42.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://memoraeu.com"
19
+ Repository = "https://github.com/pquattro/memoraeu"
20
+ Documentation = "https://docs.memoraeu.com"