@bernierllc/email 1.0.1 → 1.1.0

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 (65) hide show
  1. package/README.md +76 -217
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +28 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/simple-email-service.d.ts +58 -0
  7. package/dist/simple-email-service.d.ts.map +1 -0
  8. package/dist/simple-email-service.js +416 -0
  9. package/dist/simple-email-service.js.map +1 -0
  10. package/dist/types.d.ts +311 -0
  11. package/dist/types.d.ts.map +1 -0
  12. package/dist/types.js +33 -0
  13. package/dist/types.js.map +1 -0
  14. package/package.json +53 -22
  15. package/.eslintrc.json +0 -112
  16. package/.flake8 +0 -18
  17. package/.github/workflows/ci.yml +0 -300
  18. package/EXTRACTION_SUMMARY.md +0 -265
  19. package/IMPLEMENTATION_STATUS.md +0 -159
  20. package/OPEN_SOURCE_SETUP.md +0 -420
  21. package/PACKAGE_USAGE.md +0 -471
  22. package/examples/fastapi-example/main.py +0 -257
  23. package/examples/nextjs-example/next-env.d.ts +0 -13
  24. package/examples/nextjs-example/package.json +0 -26
  25. package/examples/nextjs-example/pages/admin/templates.tsx +0 -157
  26. package/examples/nextjs-example/tsconfig.json +0 -28
  27. package/packages/core/package.json +0 -70
  28. package/packages/core/rollup.config.js +0 -37
  29. package/packages/core/specification.md +0 -416
  30. package/packages/core/src/adapters/supabase.ts +0 -291
  31. package/packages/core/src/core/scheduler.ts +0 -356
  32. package/packages/core/src/core/template-manager.ts +0 -388
  33. package/packages/core/src/index.ts +0 -30
  34. package/packages/core/src/providers/base.ts +0 -104
  35. package/packages/core/src/providers/sendgrid.ts +0 -368
  36. package/packages/core/src/types/provider.ts +0 -91
  37. package/packages/core/src/types/scheduled.ts +0 -78
  38. package/packages/core/src/types/template.ts +0 -97
  39. package/packages/core/tsconfig.json +0 -23
  40. package/packages/python/README.md +0 -106
  41. package/packages/python/email_template_manager/__init__.py +0 -66
  42. package/packages/python/email_template_manager/config.py +0 -98
  43. package/packages/python/email_template_manager/core/magic_links.py +0 -245
  44. package/packages/python/email_template_manager/core/manager.py +0 -344
  45. package/packages/python/email_template_manager/core/scheduler.py +0 -473
  46. package/packages/python/email_template_manager/exceptions.py +0 -67
  47. package/packages/python/email_template_manager/models/magic_link.py +0 -59
  48. package/packages/python/email_template_manager/models/scheduled.py +0 -78
  49. package/packages/python/email_template_manager/models/template.py +0 -90
  50. package/packages/python/email_template_manager/providers/aws_ses.py +0 -44
  51. package/packages/python/email_template_manager/providers/base.py +0 -94
  52. package/packages/python/email_template_manager/providers/sendgrid.py +0 -325
  53. package/packages/python/email_template_manager/providers/smtp.py +0 -44
  54. package/packages/python/pyproject.toml +0 -133
  55. package/packages/python/setup.py +0 -93
  56. package/packages/python/specification.md +0 -930
  57. package/packages/react/README.md +0 -13
  58. package/packages/react/package.json +0 -105
  59. package/packages/react/rollup.config.js +0 -37
  60. package/packages/react/specification.md +0 -569
  61. package/packages/react/src/index.ts +0 -20
  62. package/packages/react/tsconfig.json +0 -24
  63. package/plans/email-template-manager_app-admin.md +0 -590
  64. package/src/index.js +0 -1
  65. package/test_package.py +0 -125
@@ -1,930 +0,0 @@
1
- # email-template-manager (Python) - Python SDK Specification
2
-
3
- ## Overview
4
-
5
- A comprehensive Python SDK for email template management, providing both low-level components and high-level integrations for FastAPI, Django, and Flask applications. Built with modern Python patterns, async support, and comprehensive type hints.
6
-
7
- ## Core Models
8
-
9
- ### Pydantic Models
10
-
11
- ```python
12
- from pydantic import BaseModel, EmailStr, Field
13
- from typing import List, Optional, Dict, Any, Literal
14
- from datetime import datetime
15
- from enum import Enum
16
-
17
- class TemplateVariable(BaseModel):
18
- name: str
19
- type: Literal['text', 'number', 'date', 'boolean', 'url']
20
- description: Optional[str] = None
21
- default_value: Optional[Any] = None
22
- required: bool = True
23
-
24
- class EmailTemplate(BaseModel):
25
- id: Optional[str] = None
26
- name: str
27
- subject: str
28
- html_body: str
29
- text_body: str
30
- variables: List[TemplateVariable] = []
31
- category_id: Optional[str] = None
32
- tags: List[str] = []
33
- is_active: bool = True
34
- version: int = 1
35
- created_at: Optional[datetime] = None
36
- updated_at: Optional[datetime] = None
37
- created_by: Optional[str] = None
38
-
39
- class ScheduledEmail(BaseModel):
40
- id: Optional[str] = None
41
- template_id: str
42
- recipient_email: EmailStr
43
- recipient_name: Optional[str] = None
44
- scheduled_for: datetime
45
- trigger_type: Literal['immediate', 'relative', 'absolute']
46
- trigger_offset: Optional[int] = None # days before/after reference date
47
- reference_date: Optional[datetime] = None
48
- variables: Dict[str, Any] = {}
49
- status: Literal['pending', 'sent', 'failed', 'cancelled', 'paused'] = 'pending'
50
- sent_at: Optional[datetime] = None
51
- error_message: Optional[str] = None
52
- retry_count: int = 0
53
- max_retries: int = 3
54
- metadata: Dict[str, Any] = {}
55
-
56
- class MagicLink(BaseModel):
57
- id: Optional[str] = None
58
- token: str
59
- email: EmailStr
60
- type: str
61
- payload: Dict[str, Any] = {}
62
- expires_at: datetime
63
- used_at: Optional[datetime] = None
64
- is_used: bool = False
65
- metadata: Dict[str, Any] = {}
66
- ```
67
-
68
- ### SQLAlchemy Models
69
-
70
- ```python
71
- from sqlalchemy import Column, String, Text, DateTime, Boolean, Integer, JSON
72
- from sqlalchemy.ext.declarative import declarative_base
73
- from sqlalchemy.sql import func
74
-
75
- Base = declarative_base()
76
-
77
- class EmailTemplateModel(Base):
78
- __tablename__ = "email_templates"
79
-
80
- id = Column(String, primary_key=True)
81
- name = Column(String, nullable=False)
82
- subject = Column(String, nullable=False)
83
- html_body = Column(Text, nullable=False)
84
- text_body = Column(Text, nullable=False)
85
- variables = Column(JSON, default=[])
86
- category_id = Column(String, nullable=True)
87
- tags = Column(JSON, default=[])
88
- is_active = Column(Boolean, default=True)
89
- version = Column(Integer, default=1)
90
- created_at = Column(DateTime, server_default=func.now())
91
- updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
92
- created_by = Column(String, nullable=True)
93
-
94
- class ScheduledEmailModel(Base):
95
- __tablename__ = "scheduled_emails"
96
-
97
- id = Column(String, primary_key=True)
98
- template_id = Column(String, nullable=False)
99
- recipient_email = Column(String, nullable=False)
100
- recipient_name = Column(String, nullable=True)
101
- scheduled_for = Column(DateTime, nullable=False)
102
- trigger_type = Column(String, nullable=False)
103
- trigger_offset = Column(Integer, nullable=True)
104
- reference_date = Column(DateTime, nullable=True)
105
- variables = Column(JSON, default={})
106
- status = Column(String, default='pending')
107
- sent_at = Column(DateTime, nullable=True)
108
- error_message = Column(Text, nullable=True)
109
- retry_count = Column(Integer, default=0)
110
- max_retries = Column(Integer, default=3)
111
- metadata = Column(JSON, default={})
112
-
113
- class MagicLinkModel(Base):
114
- __tablename__ = "magic_links"
115
-
116
- id = Column(String, primary_key=True)
117
- token = Column(String, unique=True, nullable=False)
118
- email = Column(String, nullable=False)
119
- type = Column(String, nullable=False)
120
- payload = Column(JSON, default={})
121
- expires_at = Column(DateTime, nullable=False)
122
- used_at = Column(DateTime, nullable=True)
123
- is_used = Column(Boolean, default=False)
124
- metadata = Column(JSON, default={})
125
- ```
126
-
127
- ## Core Classes
128
-
129
- ### EmailTemplateManager
130
-
131
- ```python
132
- from typing import Optional, List, Dict, Any
133
- from sqlalchemy.orm import Session
134
- from sqlalchemy import create_engine
135
-
136
- class EmailTemplateManager:
137
- def __init__(self, database_url: str, echo: bool = False):
138
- self.engine = create_engine(database_url, echo=echo)
139
- Base.metadata.create_all(self.engine)
140
-
141
- def get_session(self) -> Session:
142
- from sqlalchemy.orm import sessionmaker
143
- SessionLocal = sessionmaker(bind=self.engine)
144
- return SessionLocal()
145
-
146
- async def create_template(self, template: EmailTemplate) -> EmailTemplate:
147
- """Create a new email template"""
148
- # Implementation
149
- pass
150
-
151
- async def get_template(self, template_id: str) -> Optional[EmailTemplate]:
152
- """Get template by ID"""
153
- pass
154
-
155
- async def update_template(self, template_id: str, updates: Dict[str, Any]) -> EmailTemplate:
156
- """Update existing template"""
157
- pass
158
-
159
- async def delete_template(self, template_id: str) -> bool:
160
- """Delete template"""
161
- pass
162
-
163
- async def list_templates(
164
- self,
165
- skip: int = 0,
166
- limit: int = 100,
167
- filters: Optional[Dict[str, Any]] = None
168
- ) -> List[EmailTemplate]:
169
- """List templates with pagination and filtering"""
170
- pass
171
-
172
- async def render_template(
173
- self,
174
- template_id: str,
175
- variables: Dict[str, Any]
176
- ) -> Dict[str, str]:
177
- """Render template with variables"""
178
- pass
179
-
180
- def validate_template_variables(
181
- self,
182
- template: EmailTemplate,
183
- variables: Dict[str, Any]
184
- ) -> Dict[str, Any]:
185
- """Validate template variables"""
186
- pass
187
- ```
188
-
189
- ### EmailScheduler
190
-
191
- ```python
192
- import asyncio
193
- from datetime import datetime, timedelta
194
- from typing import List, Optional, Dict, Any
195
-
196
- class EmailScheduler:
197
- def __init__(
198
- self,
199
- template_manager: EmailTemplateManager,
200
- email_provider: 'EmailProvider',
201
- max_retries: int = 3,
202
- batch_size: int = 100
203
- ):
204
- self.template_manager = template_manager
205
- self.email_provider = email_provider
206
- self.max_retries = max_retries
207
- self.batch_size = batch_size
208
-
209
- async def schedule_email(self, email: ScheduledEmail) -> ScheduledEmail:
210
- """Schedule a single email"""
211
- pass
212
-
213
- async def schedule_batch(self, emails: List[ScheduledEmail]) -> List[ScheduledEmail]:
214
- """Schedule multiple emails"""
215
- pass
216
-
217
- async def cancel_scheduled_email(self, email_id: str) -> bool:
218
- """Cancel a scheduled email"""
219
- pass
220
-
221
- async def pause_scheduled_email(self, email_id: str) -> bool:
222
- """Pause a scheduled email"""
223
- pass
224
-
225
- async def resume_scheduled_email(self, email_id: str) -> bool:
226
- """Resume a paused email"""
227
- pass
228
-
229
- async def process_pending_emails(self) -> Dict[str, Any]:
230
- """Process all pending emails"""
231
- pass
232
-
233
- async def retry_failed_emails(self) -> Dict[str, Any]:
234
- """Retry failed emails"""
235
- pass
236
-
237
- async def get_scheduled_email(self, email_id: str) -> Optional[ScheduledEmail]:
238
- """Get scheduled email by ID"""
239
- pass
240
-
241
- async def list_scheduled_emails(
242
- self,
243
- skip: int = 0,
244
- limit: int = 100,
245
- filters: Optional[Dict[str, Any]] = None
246
- ) -> List[ScheduledEmail]:
247
- """List scheduled emails"""
248
- pass
249
- ```
250
-
251
- ### MagicLinkManager
252
-
253
- ```python
254
- import secrets
255
- import hashlib
256
- from datetime import datetime, timedelta
257
- from typing import Optional, Dict, Any
258
-
259
- class MagicLinkManager:
260
- def __init__(
261
- self,
262
- database_manager: EmailTemplateManager,
263
- encryption_key: str,
264
- default_expiry_hours: int = 24
265
- ):
266
- self.db = database_manager
267
- self.encryption_key = encryption_key
268
- self.default_expiry_hours = default_expiry_hours
269
-
270
- async def generate_magic_link(
271
- self,
272
- email: str,
273
- link_type: str,
274
- payload: Dict[str, Any],
275
- expires_in_hours: Optional[int] = None
276
- ) -> MagicLink:
277
- """Generate a new magic link"""
278
- pass
279
-
280
- async def validate_magic_link(self, token: str) -> Optional[MagicLink]:
281
- """Validate magic link token"""
282
- pass
283
-
284
- async def use_magic_link(self, token: str) -> MagicLink:
285
- """Mark magic link as used and return it"""
286
- pass
287
-
288
- async def revoke_magic_link(self, token: str) -> bool:
289
- """Revoke a magic link"""
290
- pass
291
-
292
- async def cleanup_expired_links(self) -> int:
293
- """Clean up expired magic links"""
294
- pass
295
-
296
- def _generate_token(self, length: int = 32) -> str:
297
- """Generate secure random token"""
298
- return secrets.token_urlsafe(length)
299
-
300
- def _hash_token(self, token: str) -> str:
301
- """Hash token for storage"""
302
- return hashlib.sha256(f"{token}{self.encryption_key}".encode()).hexdigest()
303
- ```
304
-
305
- ## Email Providers
306
-
307
- ### Abstract Base Provider
308
-
309
- ```python
310
- from abc import ABC, abstractmethod
311
- from typing import List, Dict, Any, Optional
312
-
313
- class EmailProvider(ABC):
314
- def __init__(self, config: Dict[str, Any]):
315
- self.config = config
316
-
317
- @abstractmethod
318
- async def send_email(
319
- self,
320
- to_email: str,
321
- subject: str,
322
- html_body: str,
323
- text_body: str,
324
- from_email: Optional[str] = None,
325
- from_name: Optional[str] = None,
326
- reply_to: Optional[str] = None,
327
- metadata: Optional[Dict[str, Any]] = None
328
- ) -> Dict[str, Any]:
329
- """Send a single email"""
330
- pass
331
-
332
- @abstractmethod
333
- async def send_batch(self, emails: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
334
- """Send multiple emails"""
335
- pass
336
-
337
- @abstractmethod
338
- async def get_delivery_status(self, message_id: str) -> Dict[str, Any]:
339
- """Get delivery status for a message"""
340
- pass
341
-
342
- @abstractmethod
343
- async def process_webhook(self, payload: Dict[str, Any], signature: str) -> List[Dict[str, Any]]:
344
- """Process webhook from email provider"""
345
- pass
346
- ```
347
-
348
- ### SendGrid Provider
349
-
350
- ```python
351
- import sendgrid
352
- from sendgrid.helpers.mail import Mail, Email, To, Content
353
-
354
- class SendGridProvider(EmailProvider):
355
- def __init__(self, config: Dict[str, Any]):
356
- super().__init__(config)
357
- self.client = sendgrid.SendGridAPIClient(api_key=config['api_key'])
358
- self.from_email = config['from_email']
359
- self.from_name = config.get('from_name', '')
360
-
361
- async def send_email(
362
- self,
363
- to_email: str,
364
- subject: str,
365
- html_body: str,
366
- text_body: str,
367
- from_email: Optional[str] = None,
368
- from_name: Optional[str] = None,
369
- reply_to: Optional[str] = None,
370
- metadata: Optional[Dict[str, Any]] = None
371
- ) -> Dict[str, Any]:
372
- """Send email using SendGrid"""
373
- # Implementation
374
- pass
375
-
376
- async def send_batch(self, emails: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
377
- """Send batch emails using SendGrid"""
378
- pass
379
-
380
- async def get_delivery_status(self, message_id: str) -> Dict[str, Any]:
381
- """Get delivery status from SendGrid"""
382
- pass
383
-
384
- async def process_webhook(self, payload: Dict[str, Any], signature: str) -> List[Dict[str, Any]]:
385
- """Process SendGrid webhook"""
386
- pass
387
- ```
388
-
389
- ### AWS SES Provider
390
-
391
- ```python
392
- import boto3
393
- from botocore.exceptions import ClientError
394
-
395
- class SESProvider(EmailProvider):
396
- def __init__(self, config: Dict[str, Any]):
397
- super().__init__(config)
398
- self.client = boto3.client(
399
- 'ses',
400
- region_name=config.get('region', 'us-east-1'),
401
- aws_access_key_id=config.get('access_key_id'),
402
- aws_secret_access_key=config.get('secret_access_key')
403
- )
404
- self.from_email = config['from_email']
405
-
406
- async def send_email(self, **kwargs) -> Dict[str, Any]:
407
- """Send email using AWS SES"""
408
- # Implementation
409
- pass
410
-
411
- # ... other methods
412
- ```
413
-
414
- ## Framework Integrations
415
-
416
- ### FastAPI Integration
417
-
418
- ```python
419
- from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
420
- from fastapi.responses import JSONResponse
421
- from typing import List, Optional
422
-
423
- def create_email_router(
424
- template_manager: EmailTemplateManager,
425
- scheduler: EmailScheduler,
426
- magic_link_manager: MagicLinkManager
427
- ):
428
- from fastapi import APIRouter
429
- router = APIRouter(prefix="/email", tags=["email"])
430
-
431
- @router.post("/templates/", response_model=EmailTemplate)
432
- async def create_template(template: EmailTemplate):
433
- """Create a new email template"""
434
- return await template_manager.create_template(template)
435
-
436
- @router.get("/templates/", response_model=List[EmailTemplate])
437
- async def list_templates(
438
- skip: int = 0,
439
- limit: int = 100,
440
- category: Optional[str] = None
441
- ):
442
- """List email templates"""
443
- filters = {"category_id": category} if category else None
444
- return await template_manager.list_templates(skip, limit, filters)
445
-
446
- @router.get("/templates/{template_id}", response_model=EmailTemplate)
447
- async def get_template(template_id: str):
448
- """Get template by ID"""
449
- template = await template_manager.get_template(template_id)
450
- if not template:
451
- raise HTTPException(status_code=404, detail="Template not found")
452
- return template
453
-
454
- @router.put("/templates/{template_id}", response_model=EmailTemplate)
455
- async def update_template(template_id: str, updates: Dict[str, Any]):
456
- """Update template"""
457
- return await template_manager.update_template(template_id, updates)
458
-
459
- @router.delete("/templates/{template_id}")
460
- async def delete_template(template_id: str):
461
- """Delete template"""
462
- success = await template_manager.delete_template(template_id)
463
- if not success:
464
- raise HTTPException(status_code=404, detail="Template not found")
465
- return {"success": True}
466
-
467
- @router.post("/schedule/", response_model=ScheduledEmail)
468
- async def schedule_email(email: ScheduledEmail, background_tasks: BackgroundTasks):
469
- """Schedule an email"""
470
- scheduled = await scheduler.schedule_email(email)
471
- if email.trigger_type == 'immediate':
472
- background_tasks.add_task(scheduler.process_pending_emails)
473
- return scheduled
474
-
475
- @router.post("/schedule/batch/", response_model=List[ScheduledEmail])
476
- async def schedule_batch(emails: List[ScheduledEmail]):
477
- """Schedule multiple emails"""
478
- return await scheduler.schedule_batch(emails)
479
-
480
- @router.post("/magic-links/generate/", response_model=MagicLink)
481
- async def generate_magic_link(
482
- email: str,
483
- link_type: str,
484
- payload: Dict[str, Any],
485
- expires_in_hours: Optional[int] = None
486
- ):
487
- """Generate magic link"""
488
- return await magic_link_manager.generate_magic_link(
489
- email, link_type, payload, expires_in_hours
490
- )
491
-
492
- @router.post("/magic-links/validate/")
493
- async def validate_magic_link(token: str):
494
- """Validate magic link"""
495
- link = await magic_link_manager.validate_magic_link(token)
496
- if not link:
497
- raise HTTPException(status_code=404, detail="Invalid or expired token")
498
- return link
499
-
500
- @router.post("/magic-links/use/")
501
- async def use_magic_link(token: str):
502
- """Use magic link"""
503
- try:
504
- link = await magic_link_manager.use_magic_link(token)
505
- return link
506
- except Exception as e:
507
- raise HTTPException(status_code=400, detail=str(e))
508
-
509
- return router
510
-
511
- # Usage example
512
- def create_app():
513
- app = FastAPI(title="Email Template Manager")
514
-
515
- # Initialize managers
516
- template_manager = EmailTemplateManager("postgresql://...")
517
- email_provider = SendGridProvider({"api_key": "...", "from_email": "..."})
518
- scheduler = EmailScheduler(template_manager, email_provider)
519
- magic_link_manager = MagicLinkManager(template_manager, "encryption_key")
520
-
521
- # Include router
522
- email_router = create_email_router(template_manager, scheduler, magic_link_manager)
523
- app.include_router(email_router)
524
-
525
- return app
526
- ```
527
-
528
- ### Django Integration
529
-
530
- ```python
531
- from django.conf import settings
532
- from django.http import JsonResponse
533
- from django.views.decorators.http import require_http_methods
534
- from django.views.decorators.csrf import csrf_exempt
535
- from django.utils.decorators import method_decorator
536
- from django.views import View
537
- import json
538
-
539
- class EmailTemplateView(View):
540
- def __init__(self):
541
- self.template_manager = EmailTemplateManager(settings.DATABASE_URL)
542
- self.scheduler = EmailScheduler(self.template_manager, self.get_email_provider())
543
-
544
- def get_email_provider(self):
545
- provider_config = settings.EMAIL_PROVIDER_CONFIG
546
- if provider_config['provider'] == 'sendgrid':
547
- return SendGridProvider(provider_config)
548
- # ... other providers
549
-
550
- @method_decorator(csrf_exempt)
551
- def dispatch(self, request, *args, **kwargs):
552
- return super().dispatch(request, *args, **kwargs)
553
-
554
- async def post(self, request):
555
- """Create email template"""
556
- data = json.loads(request.body)
557
- template = EmailTemplate(**data)
558
- created_template = await self.template_manager.create_template(template)
559
- return JsonResponse(created_template.dict())
560
-
561
- async def get(self, request):
562
- """List email templates"""
563
- templates = await self.template_manager.list_templates()
564
- return JsonResponse([t.dict() for t in templates], safe=False)
565
-
566
- # Django settings configuration
567
- EMAIL_TEMPLATE_MANAGER = {
568
- 'DATABASE_URL': 'postgresql://...',
569
- 'EMAIL_PROVIDER_CONFIG': {
570
- 'provider': 'sendgrid',
571
- 'api_key': 'your-sendgrid-api-key',
572
- 'from_email': 'your-email@domain.com',
573
- 'from_name': 'Your App'
574
- },
575
- 'MAGIC_LINK_CONFIG': {
576
- 'encryption_key': 'your-encryption-key',
577
- 'default_expiry_hours': 24
578
- }
579
- }
580
- ```
581
-
582
- ### Flask Integration
583
-
584
- ```python
585
- from flask import Flask, request, jsonify, Blueprint
586
- from typing import Dict, Any
587
-
588
- def create_email_blueprint(
589
- template_manager: EmailTemplateManager,
590
- scheduler: EmailScheduler,
591
- magic_link_manager: MagicLinkManager
592
- ) -> Blueprint:
593
- bp = Blueprint('email', __name__, url_prefix='/email')
594
-
595
- @bp.route('/templates/', methods=['POST'])
596
- async def create_template():
597
- data = request.get_json()
598
- template = EmailTemplate(**data)
599
- created_template = await template_manager.create_template(template)
600
- return jsonify(created_template.dict())
601
-
602
- @bp.route('/templates/', methods=['GET'])
603
- async def list_templates():
604
- skip = request.args.get('skip', 0, type=int)
605
- limit = request.args.get('limit', 100, type=int)
606
- templates = await template_manager.list_templates(skip, limit)
607
- return jsonify([t.dict() for t in templates])
608
-
609
- @bp.route('/schedule/', methods=['POST'])
610
- async def schedule_email():
611
- data = request.get_json()
612
- scheduled_email = ScheduledEmail(**data)
613
- result = await scheduler.schedule_email(scheduled_email)
614
- return jsonify(result.dict())
615
-
616
- return bp
617
-
618
- # Usage
619
- def create_app():
620
- app = Flask(__name__)
621
-
622
- # Initialize managers
623
- template_manager = EmailTemplateManager("postgresql://...")
624
- email_provider = SendGridProvider({"api_key": "...", "from_email": "..."})
625
- scheduler = EmailScheduler(template_manager, email_provider)
626
- magic_link_manager = MagicLinkManager(template_manager, "encryption_key")
627
-
628
- # Register blueprint
629
- email_bp = create_email_blueprint(template_manager, scheduler, magic_link_manager)
630
- app.register_blueprint(email_bp)
631
-
632
- return app
633
- ```
634
-
635
- ## Utilities and Helpers
636
-
637
- ### Template Rendering
638
-
639
- ```python
640
- from jinja2 import Environment, BaseLoader
641
- import re
642
- from typing import Dict, Any
643
-
644
- class LiquidTemplateRenderer:
645
- def __init__(self):
646
- self.env = Environment(loader=BaseLoader())
647
-
648
- def render(self, template_content: str, variables: Dict[str, Any]) -> str:
649
- """Render liquid template with variables"""
650
- template = self.env.from_string(template_content)
651
- return template.render(**variables)
652
-
653
- def validate_template(self, template_content: str) -> Dict[str, Any]:
654
- """Validate template syntax"""
655
- try:
656
- self.env.from_string(template_content)
657
- return {"valid": True, "errors": []}
658
- except Exception as e:
659
- return {"valid": False, "errors": [str(e)]}
660
-
661
- def extract_variables(self, template_content: str) -> List[str]:
662
- """Extract variable names from template"""
663
- pattern = r'\{\{\s*(\w+)\s*\}\}'
664
- return list(set(re.findall(pattern, template_content)))
665
- ```
666
-
667
- ### Validation Utilities
668
-
669
- ```python
670
- import re
671
- from typing import Dict, Any, List
672
- from email_validator import validate_email, EmailNotValidError
673
-
674
- class ValidationUtils:
675
- @staticmethod
676
- def validate_email_address(email: str) -> bool:
677
- """Validate email address format"""
678
- try:
679
- validate_email(email)
680
- return True
681
- except EmailNotValidError:
682
- return False
683
-
684
- @staticmethod
685
- def validate_template_variables(
686
- template: EmailTemplate,
687
- variables: Dict[str, Any]
688
- ) -> Dict[str, Any]:
689
- """Validate template variables against schema"""
690
- errors = []
691
- warnings = []
692
-
693
- for var_def in template.variables:
694
- if var_def.required and var_def.name not in variables:
695
- errors.append(f"Required variable '{var_def.name}' is missing")
696
- elif var_def.name in variables:
697
- value = variables[var_def.name]
698
- if not ValidationUtils._validate_variable_type(value, var_def.type):
699
- errors.append(f"Variable '{var_def.name}' has invalid type")
700
-
701
- return {
702
- "valid": len(errors) == 0,
703
- "errors": errors,
704
- "warnings": warnings
705
- }
706
-
707
- @staticmethod
708
- def _validate_variable_type(value: Any, expected_type: str) -> bool:
709
- """Validate individual variable type"""
710
- type_validators = {
711
- 'text': lambda v: isinstance(v, str),
712
- 'number': lambda v: isinstance(v, (int, float)),
713
- 'date': lambda v: isinstance(v, str) and ValidationUtils._is_valid_date(v),
714
- 'boolean': lambda v: isinstance(v, bool),
715
- 'url': lambda v: isinstance(v, str) and ValidationUtils._is_valid_url(v)
716
- }
717
-
718
- validator = type_validators.get(expected_type)
719
- return validator(value) if validator else True
720
-
721
- @staticmethod
722
- def _is_valid_date(date_str: str) -> bool:
723
- """Check if string is valid date"""
724
- try:
725
- from datetime import datetime
726
- datetime.fromisoformat(date_str.replace('Z', '+00:00'))
727
- return True
728
- except ValueError:
729
- return False
730
-
731
- @staticmethod
732
- def _is_valid_url(url: str) -> bool:
733
- """Check if string is valid URL"""
734
- url_pattern = re.compile(
735
- r'^https?://' # http:// or https://
736
- r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain...
737
- r'localhost|' # localhost...
738
- r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
739
- r'(?::\d+)?' # optional port
740
- r'(?:/?|[/?]\S+)$', re.IGNORECASE)
741
- return url_pattern.match(url) is not None
742
- ```
743
-
744
- ## CLI Tools
745
-
746
- ### Command Line Interface
747
-
748
- ```python
749
- import click
750
- import asyncio
751
- from typing import Optional
752
-
753
- @click.group()
754
- def cli():
755
- """Email Template Manager CLI"""
756
- pass
757
-
758
- @cli.command()
759
- @click.option('--database-url', required=True, help='Database connection URL')
760
- @click.option('--name', required=True, help='Template name')
761
- @click.option('--subject', required=True, help='Email subject')
762
- @click.option('--html-file', required=True, help='HTML template file')
763
- @click.option('--text-file', help='Text template file')
764
- def create_template(database_url: str, name: str, subject: str, html_file: str, text_file: Optional[str]):
765
- """Create a new email template"""
766
- async def _create():
767
- manager = EmailTemplateManager(database_url)
768
-
769
- with open(html_file, 'r') as f:
770
- html_body = f.read()
771
-
772
- text_body = ""
773
- if text_file:
774
- with open(text_file, 'r') as f:
775
- text_body = f.read()
776
-
777
- template = EmailTemplate(
778
- name=name,
779
- subject=subject,
780
- html_body=html_body,
781
- text_body=text_body
782
- )
783
-
784
- result = await manager.create_template(template)
785
- click.echo(f"Created template: {result.id}")
786
-
787
- asyncio.run(_create())
788
-
789
- @cli.command()
790
- @click.option('--database-url', required=True)
791
- def process_emails(database_url: str):
792
- """Process pending scheduled emails"""
793
- async def _process():
794
- manager = EmailTemplateManager(database_url)
795
- # Initialize email provider from config
796
- provider = SendGridProvider({"api_key": "...", "from_email": "..."})
797
- scheduler = EmailScheduler(manager, provider)
798
-
799
- result = await scheduler.process_pending_emails()
800
- click.echo(f"Processed {result['processed']} emails")
801
-
802
- asyncio.run(_process())
803
-
804
- if __name__ == '__main__':
805
- cli()
806
- ```
807
-
808
- ## Testing Framework
809
-
810
- ### Test Utilities
811
-
812
- ```python
813
- import pytest
814
- from unittest.mock import AsyncMock, MagicMock
815
- from typing import Dict, Any, Optional
816
-
817
- class MockEmailProvider(EmailProvider):
818
- def __init__(self):
819
- self.sent_emails = []
820
- self.delivery_statuses = {}
821
-
822
- async def send_email(self, **kwargs) -> Dict[str, Any]:
823
- email_data = kwargs.copy()
824
- message_id = f"mock_{len(self.sent_emails)}"
825
- email_data['message_id'] = message_id
826
- self.sent_emails.append(email_data)
827
- return {"message_id": message_id, "status": "sent"}
828
-
829
- async def send_batch(self, emails: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
830
- results = []
831
- for email in emails:
832
- result = await self.send_email(**email)
833
- results.append(result)
834
- return results
835
-
836
- async def get_delivery_status(self, message_id: str) -> Dict[str, Any]:
837
- return self.delivery_statuses.get(message_id, {"status": "delivered"})
838
-
839
- async def process_webhook(self, payload: Dict[str, Any], signature: str) -> List[Dict[str, Any]]:
840
- return [payload]
841
-
842
- @pytest.fixture
843
- def mock_email_provider():
844
- return MockEmailProvider()
845
-
846
- @pytest.fixture
847
- def template_manager():
848
- # Use in-memory SQLite for testing
849
- return EmailTemplateManager("sqlite:///:memory:")
850
-
851
- @pytest.fixture
852
- def sample_template():
853
- return EmailTemplate(
854
- name="Test Template",
855
- subject="Hello {{name}}",
856
- html_body="<h1>Hello {{name}}</h1><p>{{message}}</p>",
857
- text_body="Hello {{name}}\n\n{{message}}",
858
- variables=[
859
- TemplateVariable(name="name", type="text", required=True),
860
- TemplateVariable(name="message", type="text", required=True)
861
- ]
862
- )
863
-
864
- # Example test
865
- @pytest.mark.asyncio
866
- async def test_create_template(template_manager, sample_template):
867
- created = await template_manager.create_template(sample_template)
868
- assert created.id is not None
869
- assert created.name == sample_template.name
870
- assert created.subject == sample_template.subject
871
- ```
872
-
873
- ## Package Configuration
874
-
875
- ### setup.py
876
-
877
- ```python
878
- from setuptools import setup, find_packages
879
-
880
- setup(
881
- name="email-template-manager",
882
- version="1.0.0",
883
- description="Universal email template management library",
884
- author="Your Name",
885
- author_email="your.email@example.com",
886
- url="https://github.com/your-org/email-template-manager",
887
- packages=find_packages(),
888
- install_requires=[
889
- "pydantic>=1.8.0",
890
- "sqlalchemy>=1.4.0",
891
- "alembic>=1.7.0",
892
- "jinja2>=3.0.0",
893
- "email-validator>=1.1.0",
894
- "cryptography>=3.4.0",
895
- "click>=8.0.0",
896
- ],
897
- extras_require={
898
- "fastapi": ["fastapi>=0.68.0", "uvicorn>=0.15.0"],
899
- "django": ["django>=3.2.0"],
900
- "flask": ["flask>=2.0.0"],
901
- "sendgrid": ["sendgrid>=6.8.0"],
902
- "ses": ["boto3>=1.18.0"],
903
- "mailgun": ["requests>=2.26.0"],
904
- "postmark": ["postmarker>=0.15.0"],
905
- "test": [
906
- "pytest>=6.2.0",
907
- "pytest-asyncio>=0.15.0",
908
- "pytest-cov>=2.12.0",
909
- ]
910
- },
911
- entry_points={
912
- "console_scripts": [
913
- "email-template-manager=email_template_manager.cli:cli",
914
- ],
915
- },
916
- classifiers=[
917
- "Development Status :: 4 - Beta",
918
- "Intended Audience :: Developers",
919
- "License :: OSI Approved :: MIT License",
920
- "Programming Language :: Python :: 3",
921
- "Programming Language :: Python :: 3.8",
922
- "Programming Language :: Python :: 3.9",
923
- "Programming Language :: Python :: 3.10",
924
- "Programming Language :: Python :: 3.11",
925
- ],
926
- python_requires=">=3.8",
927
- )
928
- ```
929
-
930
- This Python SDK provides comprehensive email template management capabilities with support for major Python web frameworks and email providers.