paper-core 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.
Files changed (54) hide show
  1. paper_core-0.1.0/PKG-INFO +372 -0
  2. paper_core-0.1.0/README.md +350 -0
  3. paper_core-0.1.0/paper/core/__init__.py +0 -0
  4. paper_core-0.1.0/paper/core/audit/__init__.py +4 -0
  5. paper_core-0.1.0/paper/core/audit/audit.py +70 -0
  6. paper_core-0.1.0/paper/core/audit/enums.py +19 -0
  7. paper_core-0.1.0/paper/core/auth/__init__.py +41 -0
  8. paper_core-0.1.0/paper/core/auth/enums.py +56 -0
  9. paper_core-0.1.0/paper/core/auth/jwt.py +85 -0
  10. paper_core-0.1.0/paper/core/auth/middleware.py +179 -0
  11. paper_core-0.1.0/paper/core/auth/models.py +43 -0
  12. paper_core-0.1.0/paper/core/auth/password.py +52 -0
  13. paper_core-0.1.0/paper/core/auth/utils.py +62 -0
  14. paper_core-0.1.0/paper/core/db/__init__.py +12 -0
  15. paper_core-0.1.0/paper/core/db/base.py +70 -0
  16. paper_core-0.1.0/paper/core/db/multitenant.py +63 -0
  17. paper_core-0.1.0/paper/core/db/postgres.py +179 -0
  18. paper_core-0.1.0/paper/core/email/__init__.py +19 -0
  19. paper_core-0.1.0/paper/core/email/base.py +43 -0
  20. paper_core-0.1.0/paper/core/email/enums.py +36 -0
  21. paper_core-0.1.0/paper/core/email/service.py +47 -0
  22. paper_core-0.1.0/paper/core/email/smtp.py +131 -0
  23. paper_core-0.1.0/paper/core/email/templates/__init__.py +3 -0
  24. paper_core-0.1.0/paper/core/email/templates/account_created.py +82 -0
  25. paper_core-0.1.0/paper/core/email/templates/member_invited.py +70 -0
  26. paper_core-0.1.0/paper/core/email/templates/reset_password.py +88 -0
  27. paper_core-0.1.0/paper/core/email/theme.py +25 -0
  28. paper_core-0.1.0/paper/core/errors/__init__.py +4 -0
  29. paper_core-0.1.0/paper/core/errors/enums.py +19 -0
  30. paper_core-0.1.0/paper/core/errors/handler.py +26 -0
  31. paper_core-0.1.0/paper/core/middleware/__init__.py +9 -0
  32. paper_core-0.1.0/paper/core/middleware/hipaa.py +33 -0
  33. paper_core-0.1.0/paper/core/middleware/logging.py +54 -0
  34. paper_core-0.1.0/paper/core/middleware/request_id.py +44 -0
  35. paper_core-0.1.0/paper/core/security/__init__.py +7 -0
  36. paper_core-0.1.0/paper/core/security/base.py +53 -0
  37. paper_core-0.1.0/paper/core/security/crypto.py +118 -0
  38. paper_core-0.1.0/paper/core/security/enums.py +9 -0
  39. paper_core-0.1.0/paper/core/security/hasher.py +15 -0
  40. paper_core-0.1.0/paper/core/security/pem.py +19 -0
  41. paper_core-0.1.0/paper_core.egg-info/PKG-INFO +372 -0
  42. paper_core-0.1.0/paper_core.egg-info/SOURCES.txt +52 -0
  43. paper_core-0.1.0/paper_core.egg-info/dependency_links.txt +1 -0
  44. paper_core-0.1.0/paper_core.egg-info/requires.txt +16 -0
  45. paper_core-0.1.0/paper_core.egg-info/top_level.txt +1 -0
  46. paper_core-0.1.0/pyproject.toml +38 -0
  47. paper_core-0.1.0/setup.cfg +4 -0
  48. paper_core-0.1.0/tests/test_audit.py +134 -0
  49. paper_core-0.1.0/tests/test_auth.py +305 -0
  50. paper_core-0.1.0/tests/test_db.py +153 -0
  51. paper_core-0.1.0/tests/test_email.py +241 -0
  52. paper_core-0.1.0/tests/test_errors.py +49 -0
  53. paper_core-0.1.0/tests/test_middleware.py +144 -0
  54. paper_core-0.1.0/tests/test_security.py +98 -0
@@ -0,0 +1,372 @@
1
+ Metadata-Version: 2.4
2
+ Name: paper-core
3
+ Version: 0.1.0
4
+ Summary: Core runtime library for the PaperDraft framework
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastapi
8
+ Requires-Dist: uvicorn[standard]
9
+ Requires-Dist: pydantic
10
+ Requires-Dist: pydantic-settings
11
+ Requires-Dist: sqlalchemy[asyncio]
12
+ Requires-Dist: asyncpg
13
+ Requires-Dist: PyJWT[crypto]
14
+ Requires-Dist: argon2-cffi
15
+ Requires-Dist: cryptography
16
+ Requires-Dist: python-dotenv
17
+ Requires-Dist: python-multipart
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Requires-Dist: pytest-asyncio; extra == "dev"
21
+ Requires-Dist: httpx; extra == "dev"
22
+
23
+ # paper-core
24
+
25
+ Runtime library for the PaperDraft framework. Provides auth, database, encryption, email, middleware, error handling, and audit logging — all importable under the `paper.core` namespace.
26
+
27
+ > **Status:** pre-release · v0.1 in progress · Paper Plane Consulting LLC
28
+
29
+ ---
30
+
31
+ ## Table of Contents
32
+
33
+ 1. [Installation](#installation)
34
+ 2. [Local Development](#local-development)
35
+ 3. [Running Tests](#running-tests)
36
+ 4. [Module Reference](#module-reference)
37
+ 5. [Extending Core](#extending-core)
38
+
39
+ ---
40
+
41
+ ## Installation
42
+
43
+ ```bash
44
+ pip install paper-core
45
+ ```
46
+
47
+ **Requirements:** Python 3.11+
48
+
49
+ ---
50
+
51
+ ## Local Development
52
+
53
+ ```bash
54
+ git clone <repo-url>
55
+ cd paper-core
56
+
57
+ python -m venv .venv
58
+ source .venv/bin/activate # macOS/Linux
59
+ .venv\Scripts\activate # Windows
60
+
61
+ pip install -e ".[dev]"
62
+ ```
63
+
64
+ The `-e .` (editable install) is required — it registers the `paper` namespace package so that `from paper.core.X import Y` resolves correctly.
65
+
66
+ ---
67
+
68
+ ## Running Tests
69
+
70
+ ```bash
71
+ pytest
72
+ ```
73
+
74
+ Run a specific module:
75
+
76
+ ```bash
77
+ pytest tests/test_auth.py -v
78
+ ```
79
+
80
+ ### Test suite coverage
81
+
82
+ | File | Covers |
83
+ |---|---|
84
+ | `test_errors.py` | `ErrorHandler`, `ErrorMessage` |
85
+ | `test_security.py` | `Hasher`, `Crypto`, `RSACrypto`, `Pem`, `Encoding` |
86
+ | `test_audit.py` | `Audit`, `AuditAction`, `AuditOutcome` |
87
+ | `test_auth.py` | `Credentials`, `Signature`, `Password`, `Auth`, `Key`, `Claims`, `Authenticate`, `Authorize`, `LoginAttemptLimit` |
88
+ | `test_middleware.py` | `HipaaResponseHeaders`, `RequestIdMiddleware`, `RequestLoggingMiddleware` |
89
+ | `test_db.py` | `FilterType`, `ConnectionPoolConfig`, `_relationship_fields`, `MultiTenantPoolManager` |
90
+ | `test_email.py` | `EmailTheme`, `Info`, `Body`, `Message`, `Subject`, `MimeType` |
91
+
92
+ > **Note:** `Postgres` CRUD tests (create, retrieve, update, delete) require a live PostgreSQL connection and are not included. Run those as integration tests against a test database.
93
+
94
+ ---
95
+
96
+ ## Module Reference
97
+
98
+ ### `paper.core.auth`
99
+
100
+ JWT signing, password hashing, and FastAPI authentication dependencies.
101
+
102
+ ```python
103
+ from paper.core.auth import (
104
+ Auth, # signs JWT access + refresh token pairs
105
+ Password, # Argon2 hash and verify
106
+ Authenticate, # FastAPI dep — validates JWT from header or cookie
107
+ Authorize, # FastAPI dep — validates JWT + enforces RBAC roles
108
+ LoginAttemptLimit, # FastAPI dep — rate limits login attempts per IP
109
+ set_auth_params, # call once at startup
110
+ set_login_attempt_params,
111
+ Claims, Key, # token decode utilities
112
+ Credentials, Signature, # request/response models
113
+ Algorithm, TokenType, ClaimsKey, AuthErrorMessage,
114
+ )
115
+ ```
116
+
117
+ **Startup wiring (in `dependencies.py`):**
118
+
119
+ ```python
120
+ from paper.core.auth import set_auth_params, set_login_attempt_params
121
+ from paper.core.auth.enums import Algorithm
122
+ from datetime import timedelta
123
+
124
+ set_auth_params({
125
+ "public_key": config.ENCRYPTION.PUBLIC_KEY,
126
+ "excluded_paths": ["/auth/login", "/auth/refresh", "/health"],
127
+ "alg": Algorithm.RS256,
128
+ })
129
+
130
+ set_login_attempt_params(max_attempts=5, lockout_duration=timedelta(minutes=15))
131
+ ```
132
+
133
+ **Route usage:**
134
+
135
+ ```python
136
+ from paper.core.auth import Authenticate, Authorize
137
+
138
+ @router.get("/me")
139
+ async def me(claims = Depends(Authenticate())):
140
+ return claims
141
+
142
+ @router.delete("/{id}")
143
+ async def delete(claims = Depends(Authorize(["admin"]))):
144
+ ...
145
+ ```
146
+
147
+ ---
148
+
149
+ ### `paper.core.db`
150
+
151
+ Async SQLAlchemy PostgreSQL repository with optional multi-tenant pool management.
152
+
153
+ ```python
154
+ from paper.core.db import (
155
+ Postgres, # async SQLAlchemy repository
156
+ ConnectionPoolConfig, # pool settings
157
+ Repository, # abstract base — extend to add engines
158
+ FilterType, # EQUAL, LIKE, IN, NOT_IN, etc.
159
+ MultiTenantPoolManager, # one pool per tenant DSN
160
+ MultiTenantDbDependency, # FastAPI dep — resolves tenant DB from JWT
161
+ )
162
+ ```
163
+
164
+ **Single-tenant setup:**
165
+
166
+ ```python
167
+ from paper.core.db import Postgres, ConnectionPoolConfig
168
+
169
+ db = Postgres(
170
+ connection_string = config.POSTGRES_CONN_STRING,
171
+ config = ConnectionPoolConfig(
172
+ future=True, size=10, max_overflow=5,
173
+ recycle_after=3600, timeout=30, pre_ping=True,
174
+ ),
175
+ )
176
+ ```
177
+
178
+ **Filtering:**
179
+
180
+ ```python
181
+ results = await db.retrieve(
182
+ UserEntity, UserModel,
183
+ filter={FilterType.EQUAL: {"is_active": True}},
184
+ )
185
+ ```
186
+
187
+ ---
188
+
189
+ ### `paper.core.security`
190
+
191
+ RSA-OAEP encryption and random code generation.
192
+
193
+ ```python
194
+ from paper.core.security import Crypto, RSACrypto, Hasher, Pem, Encoding
195
+ ```
196
+
197
+ ```python
198
+ # Static utility (key passed per call)
199
+ cipher = Crypto.encrypt_urlsafe(dsn, public_key_b64)
200
+ dsn = Crypto.decrypt_urlsafe(cipher, private_key_b64)
201
+
202
+ # Instance-based (keys injected once)
203
+ crypto = RSACrypto(public_key_b64, private_key_b64)
204
+ cipher = crypto.encrypt_urlsafe(dsn)
205
+
206
+ # Random codes (invites, resets)
207
+ code = Hasher.generate(8) # e.g. "aB3xZ9mK"
208
+ ```
209
+
210
+ **Encoding PEM keys for `.env` storage:**
211
+
212
+ ```bash
213
+ openssl genrsa -out private.pem 2048
214
+ openssl rsa -in private.pem -pubout -out public.pem
215
+ base64 -w 0 private.pem # → ENCRYPTION_PRIVATE_KEY
216
+ base64 -w 0 public.pem # → ENCRYPTION_PUBLIC_KEY
217
+ ```
218
+
219
+ Or with `Pem`:
220
+
221
+ ```python
222
+ from paper.core.security import Pem
223
+ print(Pem.to_base64("private.pem"))
224
+ ```
225
+
226
+ ---
227
+
228
+ ### `paper.core.middleware`
229
+
230
+ ```python
231
+ from paper.core.middleware import (
232
+ HipaaResponseHeaders, # X-Frame-Options, CSP, HSTS, etc.
233
+ RequestIdMiddleware, # X-Request-ID on every request
234
+ RequestLoggingMiddleware, # structured request/response logging
235
+ )
236
+ ```
237
+
238
+ **Registration order in `main.py`:**
239
+
240
+ ```python
241
+ app.add_middleware(CORSMiddleware, ...)
242
+ app.add_middleware(HipaaResponseHeaders)
243
+ app.add_middleware(RequestLoggingMiddleware)
244
+ app.add_middleware(RequestIdMiddleware) # executes first
245
+ ```
246
+
247
+ Access the request ID downstream:
248
+
249
+ ```python
250
+ request.state.request_id
251
+ ```
252
+
253
+ ---
254
+
255
+ ### `paper.core.errors`
256
+
257
+ ```python
258
+ from paper.core.errors import ErrorHandler, ErrorMessage
259
+
260
+ ErrorHandler.handle(404, f"{ErrorMessage.NOT_FOUND.value} user {id}")
261
+ ErrorHandler.handle(401, "Unauthorized", {"WWW-Authenticate": "Bearer"})
262
+ ```
263
+
264
+ All service layers raise through `ErrorHandler` — never construct `HTTPException` directly.
265
+
266
+ ---
267
+
268
+ ### `paper.core.audit`
269
+
270
+ Generic audit logger. Accepts any DB, entity, and model — no framework-level schema dependency.
271
+
272
+ ```python
273
+ from paper.core.audit import Audit, AuditAction, AuditOutcome
274
+
275
+ audit = Audit(db=db, entity=AuditLogEntity, model=AuditLogModel)
276
+
277
+ await audit.log(
278
+ event_type = LoginEvent.SUCCESS,
279
+ action = AuditAction.ATTEMPT,
280
+ outcome = AuditOutcome.SUCCESS,
281
+ email = credentials.email,
282
+ ip_address = request.client.host,
283
+ user_agent = request.headers.get("user-agent"),
284
+ )
285
+ ```
286
+
287
+ Audit failures are swallowed silently and logged — they never block the calling operation.
288
+
289
+ ---
290
+
291
+ ### `paper.core.email`
292
+
293
+ SMTP email with framework lifecycle templates and themeable HTML.
294
+
295
+ ```python
296
+ from paper.core.email import (
297
+ Server, Info, Body, Message,
298
+ Subject, EmailTheme, EmailBodyParam,
299
+ )
300
+
301
+ server = Server(host, port, username, password)
302
+ sender = Info("My App", "noreply@myapp.com")
303
+
304
+ body = Body(
305
+ subject = Subject.RESET_PASSWORD,
306
+ data = {
307
+ EmailBodyParam.REDIRECT_URL.value: reset_url,
308
+ EmailBodyParam.RESET_CODE.value: code,
309
+ },
310
+ )
311
+
312
+ msg = Message(
313
+ sender = sender,
314
+ recipient = Info(user.name, user.email),
315
+ subject = Subject.RESET_PASSWORD.value,
316
+ text = body.text,
317
+ html = body.html,
318
+ )
319
+
320
+ server.send(msg)
321
+ ```
322
+
323
+ **Theming via environment variables:**
324
+
325
+ ```env
326
+ EMAIL_THEME_PRIMARY_COLOR=#0057a8
327
+ EMAIL_THEME_BUTTON_COLOR=#0057a8
328
+ EMAIL_THEME_LOGO_URL=https://cdn.myapp.com/logo.png
329
+ EMAIL_THEME_COMPANY_NAME=My App
330
+ ```
331
+
332
+ ---
333
+
334
+ ## Extending Core
335
+
336
+ All major components are built on abstract base classes — extend them to swap implementations without changing the calling code.
337
+
338
+ ### Custom Database Engine
339
+
340
+ ```python
341
+ from paper.core.db.base import Repository, FilterType
342
+
343
+ class MySQLRepository(Repository[T, M]):
344
+ async def create(self, entity, model, data): ...
345
+ async def retrieve(self, entity, model, filter=None): ...
346
+ async def single(self, entity, model, id): ...
347
+ async def update(self, entity, model, id, data): ...
348
+ async def delete(self, entity, id): ...
349
+ ```
350
+
351
+ ### Custom Encryption Algorithm
352
+
353
+ ```python
354
+ from paper.core.security.base import BaseCrypto
355
+
356
+ class AESCrypto(BaseCrypto):
357
+ def encrypt(self, value: str) -> str: ...
358
+ def decrypt(self, cipher: str) -> str: ...
359
+ def encrypt_urlsafe(self, value: str) -> str: ...
360
+ def decrypt_urlsafe(self, cipher: str) -> str: ...
361
+ def encrypt_raw(self, value: str) -> bytes: ...
362
+ def decrypt_raw(self, cipher_bytes: bytes) -> str: ...
363
+ ```
364
+
365
+ ### Custom Email Provider
366
+
367
+ ```python
368
+ from paper.core.email.base import BaseEmailService
369
+
370
+ class SendGridEmailService(BaseEmailService):
371
+ def send(self, subject, recipient_name, recipient_email, data) -> bool: ...
372
+ ```
@@ -0,0 +1,350 @@
1
+ # paper-core
2
+
3
+ Runtime library for the PaperDraft framework. Provides auth, database, encryption, email, middleware, error handling, and audit logging — all importable under the `paper.core` namespace.
4
+
5
+ > **Status:** pre-release · v0.1 in progress · Paper Plane Consulting LLC
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ 1. [Installation](#installation)
12
+ 2. [Local Development](#local-development)
13
+ 3. [Running Tests](#running-tests)
14
+ 4. [Module Reference](#module-reference)
15
+ 5. [Extending Core](#extending-core)
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install paper-core
23
+ ```
24
+
25
+ **Requirements:** Python 3.11+
26
+
27
+ ---
28
+
29
+ ## Local Development
30
+
31
+ ```bash
32
+ git clone <repo-url>
33
+ cd paper-core
34
+
35
+ python -m venv .venv
36
+ source .venv/bin/activate # macOS/Linux
37
+ .venv\Scripts\activate # Windows
38
+
39
+ pip install -e ".[dev]"
40
+ ```
41
+
42
+ The `-e .` (editable install) is required — it registers the `paper` namespace package so that `from paper.core.X import Y` resolves correctly.
43
+
44
+ ---
45
+
46
+ ## Running Tests
47
+
48
+ ```bash
49
+ pytest
50
+ ```
51
+
52
+ Run a specific module:
53
+
54
+ ```bash
55
+ pytest tests/test_auth.py -v
56
+ ```
57
+
58
+ ### Test suite coverage
59
+
60
+ | File | Covers |
61
+ |---|---|
62
+ | `test_errors.py` | `ErrorHandler`, `ErrorMessage` |
63
+ | `test_security.py` | `Hasher`, `Crypto`, `RSACrypto`, `Pem`, `Encoding` |
64
+ | `test_audit.py` | `Audit`, `AuditAction`, `AuditOutcome` |
65
+ | `test_auth.py` | `Credentials`, `Signature`, `Password`, `Auth`, `Key`, `Claims`, `Authenticate`, `Authorize`, `LoginAttemptLimit` |
66
+ | `test_middleware.py` | `HipaaResponseHeaders`, `RequestIdMiddleware`, `RequestLoggingMiddleware` |
67
+ | `test_db.py` | `FilterType`, `ConnectionPoolConfig`, `_relationship_fields`, `MultiTenantPoolManager` |
68
+ | `test_email.py` | `EmailTheme`, `Info`, `Body`, `Message`, `Subject`, `MimeType` |
69
+
70
+ > **Note:** `Postgres` CRUD tests (create, retrieve, update, delete) require a live PostgreSQL connection and are not included. Run those as integration tests against a test database.
71
+
72
+ ---
73
+
74
+ ## Module Reference
75
+
76
+ ### `paper.core.auth`
77
+
78
+ JWT signing, password hashing, and FastAPI authentication dependencies.
79
+
80
+ ```python
81
+ from paper.core.auth import (
82
+ Auth, # signs JWT access + refresh token pairs
83
+ Password, # Argon2 hash and verify
84
+ Authenticate, # FastAPI dep — validates JWT from header or cookie
85
+ Authorize, # FastAPI dep — validates JWT + enforces RBAC roles
86
+ LoginAttemptLimit, # FastAPI dep — rate limits login attempts per IP
87
+ set_auth_params, # call once at startup
88
+ set_login_attempt_params,
89
+ Claims, Key, # token decode utilities
90
+ Credentials, Signature, # request/response models
91
+ Algorithm, TokenType, ClaimsKey, AuthErrorMessage,
92
+ )
93
+ ```
94
+
95
+ **Startup wiring (in `dependencies.py`):**
96
+
97
+ ```python
98
+ from paper.core.auth import set_auth_params, set_login_attempt_params
99
+ from paper.core.auth.enums import Algorithm
100
+ from datetime import timedelta
101
+
102
+ set_auth_params({
103
+ "public_key": config.ENCRYPTION.PUBLIC_KEY,
104
+ "excluded_paths": ["/auth/login", "/auth/refresh", "/health"],
105
+ "alg": Algorithm.RS256,
106
+ })
107
+
108
+ set_login_attempt_params(max_attempts=5, lockout_duration=timedelta(minutes=15))
109
+ ```
110
+
111
+ **Route usage:**
112
+
113
+ ```python
114
+ from paper.core.auth import Authenticate, Authorize
115
+
116
+ @router.get("/me")
117
+ async def me(claims = Depends(Authenticate())):
118
+ return claims
119
+
120
+ @router.delete("/{id}")
121
+ async def delete(claims = Depends(Authorize(["admin"]))):
122
+ ...
123
+ ```
124
+
125
+ ---
126
+
127
+ ### `paper.core.db`
128
+
129
+ Async SQLAlchemy PostgreSQL repository with optional multi-tenant pool management.
130
+
131
+ ```python
132
+ from paper.core.db import (
133
+ Postgres, # async SQLAlchemy repository
134
+ ConnectionPoolConfig, # pool settings
135
+ Repository, # abstract base — extend to add engines
136
+ FilterType, # EQUAL, LIKE, IN, NOT_IN, etc.
137
+ MultiTenantPoolManager, # one pool per tenant DSN
138
+ MultiTenantDbDependency, # FastAPI dep — resolves tenant DB from JWT
139
+ )
140
+ ```
141
+
142
+ **Single-tenant setup:**
143
+
144
+ ```python
145
+ from paper.core.db import Postgres, ConnectionPoolConfig
146
+
147
+ db = Postgres(
148
+ connection_string = config.POSTGRES_CONN_STRING,
149
+ config = ConnectionPoolConfig(
150
+ future=True, size=10, max_overflow=5,
151
+ recycle_after=3600, timeout=30, pre_ping=True,
152
+ ),
153
+ )
154
+ ```
155
+
156
+ **Filtering:**
157
+
158
+ ```python
159
+ results = await db.retrieve(
160
+ UserEntity, UserModel,
161
+ filter={FilterType.EQUAL: {"is_active": True}},
162
+ )
163
+ ```
164
+
165
+ ---
166
+
167
+ ### `paper.core.security`
168
+
169
+ RSA-OAEP encryption and random code generation.
170
+
171
+ ```python
172
+ from paper.core.security import Crypto, RSACrypto, Hasher, Pem, Encoding
173
+ ```
174
+
175
+ ```python
176
+ # Static utility (key passed per call)
177
+ cipher = Crypto.encrypt_urlsafe(dsn, public_key_b64)
178
+ dsn = Crypto.decrypt_urlsafe(cipher, private_key_b64)
179
+
180
+ # Instance-based (keys injected once)
181
+ crypto = RSACrypto(public_key_b64, private_key_b64)
182
+ cipher = crypto.encrypt_urlsafe(dsn)
183
+
184
+ # Random codes (invites, resets)
185
+ code = Hasher.generate(8) # e.g. "aB3xZ9mK"
186
+ ```
187
+
188
+ **Encoding PEM keys for `.env` storage:**
189
+
190
+ ```bash
191
+ openssl genrsa -out private.pem 2048
192
+ openssl rsa -in private.pem -pubout -out public.pem
193
+ base64 -w 0 private.pem # → ENCRYPTION_PRIVATE_KEY
194
+ base64 -w 0 public.pem # → ENCRYPTION_PUBLIC_KEY
195
+ ```
196
+
197
+ Or with `Pem`:
198
+
199
+ ```python
200
+ from paper.core.security import Pem
201
+ print(Pem.to_base64("private.pem"))
202
+ ```
203
+
204
+ ---
205
+
206
+ ### `paper.core.middleware`
207
+
208
+ ```python
209
+ from paper.core.middleware import (
210
+ HipaaResponseHeaders, # X-Frame-Options, CSP, HSTS, etc.
211
+ RequestIdMiddleware, # X-Request-ID on every request
212
+ RequestLoggingMiddleware, # structured request/response logging
213
+ )
214
+ ```
215
+
216
+ **Registration order in `main.py`:**
217
+
218
+ ```python
219
+ app.add_middleware(CORSMiddleware, ...)
220
+ app.add_middleware(HipaaResponseHeaders)
221
+ app.add_middleware(RequestLoggingMiddleware)
222
+ app.add_middleware(RequestIdMiddleware) # executes first
223
+ ```
224
+
225
+ Access the request ID downstream:
226
+
227
+ ```python
228
+ request.state.request_id
229
+ ```
230
+
231
+ ---
232
+
233
+ ### `paper.core.errors`
234
+
235
+ ```python
236
+ from paper.core.errors import ErrorHandler, ErrorMessage
237
+
238
+ ErrorHandler.handle(404, f"{ErrorMessage.NOT_FOUND.value} user {id}")
239
+ ErrorHandler.handle(401, "Unauthorized", {"WWW-Authenticate": "Bearer"})
240
+ ```
241
+
242
+ All service layers raise through `ErrorHandler` — never construct `HTTPException` directly.
243
+
244
+ ---
245
+
246
+ ### `paper.core.audit`
247
+
248
+ Generic audit logger. Accepts any DB, entity, and model — no framework-level schema dependency.
249
+
250
+ ```python
251
+ from paper.core.audit import Audit, AuditAction, AuditOutcome
252
+
253
+ audit = Audit(db=db, entity=AuditLogEntity, model=AuditLogModel)
254
+
255
+ await audit.log(
256
+ event_type = LoginEvent.SUCCESS,
257
+ action = AuditAction.ATTEMPT,
258
+ outcome = AuditOutcome.SUCCESS,
259
+ email = credentials.email,
260
+ ip_address = request.client.host,
261
+ user_agent = request.headers.get("user-agent"),
262
+ )
263
+ ```
264
+
265
+ Audit failures are swallowed silently and logged — they never block the calling operation.
266
+
267
+ ---
268
+
269
+ ### `paper.core.email`
270
+
271
+ SMTP email with framework lifecycle templates and themeable HTML.
272
+
273
+ ```python
274
+ from paper.core.email import (
275
+ Server, Info, Body, Message,
276
+ Subject, EmailTheme, EmailBodyParam,
277
+ )
278
+
279
+ server = Server(host, port, username, password)
280
+ sender = Info("My App", "noreply@myapp.com")
281
+
282
+ body = Body(
283
+ subject = Subject.RESET_PASSWORD,
284
+ data = {
285
+ EmailBodyParam.REDIRECT_URL.value: reset_url,
286
+ EmailBodyParam.RESET_CODE.value: code,
287
+ },
288
+ )
289
+
290
+ msg = Message(
291
+ sender = sender,
292
+ recipient = Info(user.name, user.email),
293
+ subject = Subject.RESET_PASSWORD.value,
294
+ text = body.text,
295
+ html = body.html,
296
+ )
297
+
298
+ server.send(msg)
299
+ ```
300
+
301
+ **Theming via environment variables:**
302
+
303
+ ```env
304
+ EMAIL_THEME_PRIMARY_COLOR=#0057a8
305
+ EMAIL_THEME_BUTTON_COLOR=#0057a8
306
+ EMAIL_THEME_LOGO_URL=https://cdn.myapp.com/logo.png
307
+ EMAIL_THEME_COMPANY_NAME=My App
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Extending Core
313
+
314
+ All major components are built on abstract base classes — extend them to swap implementations without changing the calling code.
315
+
316
+ ### Custom Database Engine
317
+
318
+ ```python
319
+ from paper.core.db.base import Repository, FilterType
320
+
321
+ class MySQLRepository(Repository[T, M]):
322
+ async def create(self, entity, model, data): ...
323
+ async def retrieve(self, entity, model, filter=None): ...
324
+ async def single(self, entity, model, id): ...
325
+ async def update(self, entity, model, id, data): ...
326
+ async def delete(self, entity, id): ...
327
+ ```
328
+
329
+ ### Custom Encryption Algorithm
330
+
331
+ ```python
332
+ from paper.core.security.base import BaseCrypto
333
+
334
+ class AESCrypto(BaseCrypto):
335
+ def encrypt(self, value: str) -> str: ...
336
+ def decrypt(self, cipher: str) -> str: ...
337
+ def encrypt_urlsafe(self, value: str) -> str: ...
338
+ def decrypt_urlsafe(self, cipher: str) -> str: ...
339
+ def encrypt_raw(self, value: str) -> bytes: ...
340
+ def decrypt_raw(self, cipher_bytes: bytes) -> str: ...
341
+ ```
342
+
343
+ ### Custom Email Provider
344
+
345
+ ```python
346
+ from paper.core.email.base import BaseEmailService
347
+
348
+ class SendGridEmailService(BaseEmailService):
349
+ def send(self, subject, recipient_name, recipient_email, data) -> bool: ...
350
+ ```
File without changes
@@ -0,0 +1,4 @@
1
+ from paper.core.audit.audit import Audit
2
+ from paper.core.audit.enums import AuditAction, AuditOutcome
3
+
4
+ __all__ = ["Audit", "AuditAction", "AuditOutcome"]