utkit 0.4.0__tar.gz → 0.6.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 (85) hide show
  1. utkit-0.6.0/PKG-INFO +106 -0
  2. utkit-0.6.0/README.md +77 -0
  3. utkit-0.6.0/pyproject.toml +44 -0
  4. utkit-0.6.0/src/utkit/__init__.py +6 -0
  5. utkit-0.6.0/src/utkit/api/rate_limit/__init__.py +9 -0
  6. utkit-0.6.0/src/utkit/api/rate_limit/error.py +4 -0
  7. utkit-0.6.0/src/utkit/api/rate_limit/middleware.py +4 -0
  8. utkit-0.6.0/src/utkit/api/schema/query.py +7 -0
  9. utkit-0.6.0/src/utkit/auth/__init__.py +16 -0
  10. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/cli/__init__.py +52 -52
  11. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/communication/mail/smtp.py +63 -63
  12. utkit-0.6.0/src/utkit/core/logging.py +88 -0
  13. utkit-0.6.0/src/utkit/documentation/.cache/10042715452264249772 +4 -0
  14. utkit-0.6.0/src/utkit/documentation/.cache/11005013185016532949 +4 -0
  15. utkit-0.6.0/src/utkit/documentation/.cache/11038628458439117958 +4 -0
  16. utkit-0.6.0/src/utkit/documentation/.cache/11306067049371264785 +4 -0
  17. utkit-0.6.0/src/utkit/documentation/.cache/11523282269049922043 +111 -0
  18. utkit-0.6.0/src/utkit/documentation/.cache/13386297405578974681 +4 -0
  19. utkit-0.6.0/src/utkit/documentation/.cache/15312684519617069940 +128 -0
  20. utkit-0.6.0/src/utkit/documentation/.cache/16166828772222643061 +127 -0
  21. utkit-0.6.0/src/utkit/documentation/.cache/17695336329032716848 +4 -0
  22. utkit-0.6.0/src/utkit/documentation/.cache/17985671397626253671 +192 -0
  23. utkit-0.6.0/src/utkit/documentation/.cache/2940665334038842907 +307 -0
  24. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/documentation/.cache/326316130724058699 +1 -1
  25. utkit-0.6.0/src/utkit/documentation/.cache/3476900567878811119 +4 -0
  26. utkit-0.6.0/src/utkit/documentation/.cache/3541202890535126406 +259 -0
  27. utkit-0.6.0/src/utkit/documentation/.cache/360188204203718330 +4 -0
  28. utkit-0.6.0/src/utkit/documentation/.cache/403326901327389294 +143 -0
  29. utkit-0.6.0/src/utkit/documentation/.cache/5520961796158849726 +4 -0
  30. utkit-0.6.0/src/utkit/documentation/.cache/5530722400779903482 +4 -0
  31. utkit-0.6.0/src/utkit/documentation/.cache/5696925338617156769 +111 -0
  32. utkit-0.6.0/src/utkit/documentation/.cache/5974781288462474643 +4 -0
  33. utkit-0.6.0/src/utkit/documentation/.cache/5976522500417319979 +111 -0
  34. utkit-0.6.0/src/utkit/documentation/.cache/6210600303880993421 +96 -0
  35. utkit-0.6.0/src/utkit/documentation/.cache/6916343365748484443 +111 -0
  36. utkit-0.6.0/src/utkit/documentation/.cache/8439994065325268387 +258 -0
  37. utkit-0.6.0/src/utkit/documentation/.cache/9033698058957892258 +4 -0
  38. utkit-0.6.0/src/utkit/documentation/.cache/9811225078116513287 +4 -0
  39. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/documentation/.github/workflows/docs.yml +29 -29
  40. utkit-0.6.0/src/utkit/documentation/docs/api/rate-limit.md +120 -0
  41. utkit-0.6.0/src/utkit/documentation/docs/api/schema.md +73 -0
  42. utkit-0.6.0/src/utkit/documentation/docs/auth.md +98 -0
  43. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/documentation/docs/communication/mail.md +143 -143
  44. utkit-0.6.0/src/utkit/documentation/docs/images/logo-old.png +0 -0
  45. utkit-0.6.0/src/utkit/documentation/docs/images/logo.png +0 -0
  46. utkit-0.6.0/src/utkit/documentation/docs/index.md +207 -0
  47. utkit-0.6.0/src/utkit/documentation/docs/privacy/mask.md +128 -0
  48. utkit-0.6.0/src/utkit/documentation/docs/privacy/security.md +299 -0
  49. utkit-0.6.0/src/utkit/documentation/docs/store/redis.md +187 -0
  50. utkit-0.6.0/src/utkit/documentation/docs/stylesheets/extra.css +15 -0
  51. utkit-0.6.0/src/utkit/documentation/docs/template/render.md +138 -0
  52. utkit-0.6.0/src/utkit/documentation/docs/utils/performance.md +117 -0
  53. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/documentation/zensical.toml +372 -351
  54. utkit-0.6.0/src/utkit/privacy/mask.py +57 -0
  55. utkit-0.6.0/src/utkit/privacy/security.py +167 -0
  56. utkit-0.6.0/src/utkit/store/redis/__init__.py +50 -0
  57. utkit-0.6.0/src/utkit/template/render.py +35 -0
  58. utkit-0.6.0/src/utkit/utils/performance.py +35 -0
  59. utkit-0.4.0/PKG-INFO +0 -11
  60. utkit-0.4.0/pyproject.toml +0 -20
  61. utkit-0.4.0/src/utkit/__init__.py +0 -2
  62. utkit-0.4.0/src/utkit/documentation/.cache/15661711846603463071 +0 -207
  63. utkit-0.4.0/src/utkit/documentation/.cache/3226154870166803660 +0 -4
  64. utkit-0.4.0/src/utkit/documentation/.cache/3476900567878811119 +0 -4
  65. utkit-0.4.0/src/utkit/documentation/.cache/3541202890535126406 +0 -95
  66. utkit-0.4.0/src/utkit/documentation/.cache/9033698058957892258 +0 -4
  67. utkit-0.4.0/src/utkit/documentation/docs/index.md +0 -68
  68. utkit-0.4.0/src/utkit/documentation/site/assets/images/favicon.png +0 -0
  69. utkit-0.4.0/src/utkit/documentation/site/assets/javascripts/LICENSE +0 -29
  70. utkit-0.4.0/src/utkit/documentation/site/assets/javascripts/bundle.dbc0afdc.min.js +0 -3
  71. utkit-0.4.0/src/utkit/documentation/site/assets/javascripts/workers/search.e2d2d235.min.js +0 -1
  72. utkit-0.4.0/src/utkit/documentation/site/assets/stylesheets/classic/main.a2001754.min.css +0 -1
  73. utkit-0.4.0/src/utkit/documentation/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css +0 -1
  74. utkit-0.4.0/src/utkit/documentation/site/assets/stylesheets/modern/main.1e981d71.min.css +0 -1
  75. utkit-0.4.0/src/utkit/documentation/site/assets/stylesheets/modern/palette.dfe2e883.min.css +0 -1
  76. utkit-0.4.0/src/utkit/documentation/site/communication/mail/index.html +0 -995
  77. utkit-0.4.0/src/utkit/documentation/site/index.html +0 -697
  78. utkit-0.4.0/src/utkit/documentation/site/search.json +0 -1
  79. utkit-0.4.0/src/utkit/documentation/site/sitemap.xml +0 -13
  80. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/communication/__init__.py +0 -0
  81. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/communication/mail/__init__.py +0 -0
  82. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/documentation/.cache/.gitignore +0 -0
  83. {utkit-0.4.0 → utkit-0.6.0}/src/utkit/documentation/.gitignore +0 -0
  84. /utkit-0.4.0/README.md → /utkit-0.6.0/src/utkit/privacy/__init__.py +0 -0
  85. /utkit-0.4.0/src/utkit/documentation/site/objects.inv → /utkit-0.6.0/src/utkit/template/__init__.py +0 -0
utkit-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.3
2
+ Name: utkit
3
+ Version: 0.6.0
4
+ Summary: core libraries for development
5
+ Author: TINS PJ
6
+ Author-email: TINS PJ <tinspj1997@gmail.com>
7
+ Requires-Dist: cryptography>=47.0.0
8
+ Requires-Dist: loguru>=0.7.3
9
+ Requires-Dist: pwdlib[argon2]>=0.3.0
10
+ Requires-Dist: pyjwt>=2.12.1
11
+ Requires-Dist: typer>=0.20.0
12
+ Requires-Dist: zensical>=0.0.37
13
+ Requires-Dist: slowapi>=0.1.9 ; extra == 'all'
14
+ Requires-Dist: pydantic>=2.13.3 ; extra == 'all'
15
+ Requires-Dist: psutil>=7.2.2 ; extra == 'all'
16
+ Requires-Dist: jinja2>=3.1.6 ; extra == 'all'
17
+ Requires-Dist: redis>=7.4.0 ; extra == 'all'
18
+ Requires-Dist: slowapi>=0.1.9 ; extra == 'api'
19
+ Requires-Dist: pydantic>=2.13.3 ; extra == 'api'
20
+ Requires-Dist: psutil>=7.2.2 ; extra == 'standard'
21
+ Requires-Dist: jinja2>=3.1.6 ; extra == 'standard'
22
+ Requires-Dist: redis>=7.4.0 ; extra == 'store'
23
+ Requires-Python: >=3.12
24
+ Provides-Extra: all
25
+ Provides-Extra: api
26
+ Provides-Extra: standard
27
+ Provides-Extra: store
28
+ Description-Content-Type: text/markdown
29
+
30
+ # utkit
31
+
32
+ <div style="text-align: center; margin: 1.5rem 0;">
33
+ <img src="https://res.cloudinary.com/daft06bly/image/upload/v1777439163/logo_wussdc.png" alt="utkit" style="height: 100px; width: auto;" />
34
+ </div>
35
+
36
+ **utkit** is a collection of core libraries for Python development, providing ready-to-use utilities for common tasks such as authentication, email, encryption, caching, rate limiting, and more.
37
+
38
+
39
+ - **Author:** TINS P JOSEPH
40
+ - **Requires:** Python `>=3.12`
41
+ - **License:** MIT
42
+ - **PyPI:** [pypi.org/project/utkit](https://pypi.org/project/utkit/)
43
+
44
+ ---
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install utkit
50
+ ```
51
+
52
+ Or with [uv](https://docs.astral.sh/uv/):
53
+
54
+ ```bash
55
+ uv add utkit
56
+ ```
57
+
58
+ ### Optional extras
59
+
60
+ | Extra | Included dependencies | Use for |
61
+ |---|---|---|
62
+ | `api` | `slowapi`, `pydantic` | Rate limiting and API schema for FastAPI |
63
+ | `standard` | `psutil`, `jinja2` | Performance monitoring and HTML template rendering |
64
+ | `store` | `redis` | Redis cache and key-value store |
65
+ | `all` | all of the above | Install every optional dependency at once |
66
+
67
+ ```bash
68
+ pip install "utkit[api]"
69
+ pip install "utkit[standard]"
70
+ pip install "utkit[store]"
71
+
72
+ # All optional dependencies at once
73
+ pip install "utkit[all]"
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Modules
79
+
80
+ ### Core
81
+
82
+ | Module | Description |
83
+ |---|---|
84
+ | `auth` | Password hashing and verification |
85
+ | `communication.mail` | Send HTML emails via SMTP |
86
+ | `privacy.mask` | Mask sensitive data (email, phone, card, string) |
87
+ | `privacy.security` | Fernet & RSA encryption, secret key generation, JWT |
88
+
89
+ ### Optional
90
+
91
+ | Module | Extra | Description |
92
+ |---|---|---|
93
+ | `api.rate_limit` | `api` | Rate limiting for FastAPI via SlowAPI |
94
+ | `api.schema` | `api` | Reusable Pydantic query models (pagination) |
95
+ | `utils.performance` | `standard` | Execution time decorator and memory usage |
96
+ | `template.render` | `standard` | HTML rendering from Jinja2 files and strings |
97
+ | `store.redis` | `store` | Singleton Redis client with JSON serialisation |
98
+
99
+ ---
100
+
101
+ ## CLI
102
+
103
+ ```bash
104
+ utkit --version
105
+ utkit docs
106
+ ```
utkit-0.6.0/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # utkit
2
+
3
+ <div style="text-align: center; margin: 1.5rem 0;">
4
+ <img src="https://res.cloudinary.com/daft06bly/image/upload/v1777439163/logo_wussdc.png" alt="utkit" style="height: 100px; width: auto;" />
5
+ </div>
6
+
7
+ **utkit** is a collection of core libraries for Python development, providing ready-to-use utilities for common tasks such as authentication, email, encryption, caching, rate limiting, and more.
8
+
9
+
10
+ - **Author:** TINS P JOSEPH
11
+ - **Requires:** Python `>=3.12`
12
+ - **License:** MIT
13
+ - **PyPI:** [pypi.org/project/utkit](https://pypi.org/project/utkit/)
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install utkit
21
+ ```
22
+
23
+ Or with [uv](https://docs.astral.sh/uv/):
24
+
25
+ ```bash
26
+ uv add utkit
27
+ ```
28
+
29
+ ### Optional extras
30
+
31
+ | Extra | Included dependencies | Use for |
32
+ |---|---|---|
33
+ | `api` | `slowapi`, `pydantic` | Rate limiting and API schema for FastAPI |
34
+ | `standard` | `psutil`, `jinja2` | Performance monitoring and HTML template rendering |
35
+ | `store` | `redis` | Redis cache and key-value store |
36
+ | `all` | all of the above | Install every optional dependency at once |
37
+
38
+ ```bash
39
+ pip install "utkit[api]"
40
+ pip install "utkit[standard]"
41
+ pip install "utkit[store]"
42
+
43
+ # All optional dependencies at once
44
+ pip install "utkit[all]"
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Modules
50
+
51
+ ### Core
52
+
53
+ | Module | Description |
54
+ |---|---|
55
+ | `auth` | Password hashing and verification |
56
+ | `communication.mail` | Send HTML emails via SMTP |
57
+ | `privacy.mask` | Mask sensitive data (email, phone, card, string) |
58
+ | `privacy.security` | Fernet & RSA encryption, secret key generation, JWT |
59
+
60
+ ### Optional
61
+
62
+ | Module | Extra | Description |
63
+ |---|---|---|
64
+ | `api.rate_limit` | `api` | Rate limiting for FastAPI via SlowAPI |
65
+ | `api.schema` | `api` | Reusable Pydantic query models (pagination) |
66
+ | `utils.performance` | `standard` | Execution time decorator and memory usage |
67
+ | `template.render` | `standard` | HTML rendering from Jinja2 files and strings |
68
+ | `store.redis` | `store` | Singleton Redis client with JSON serialisation |
69
+
70
+ ---
71
+
72
+ ## CLI
73
+
74
+ ```bash
75
+ utkit --version
76
+ utkit docs
77
+ ```
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "utkit"
3
+ version = "0.6.0"
4
+ description = "core libraries for development"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "TINS PJ", email = "tinspj1997@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "cryptography>=47.0.0",
12
+ "loguru>=0.7.3",
13
+ "pwdlib[argon2]>=0.3.0",
14
+ "pyjwt>=2.12.1",
15
+ "typer>=0.20.0",
16
+ "zensical>=0.0.37",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ api=[
21
+ "slowapi>=0.1.9",
22
+ "pydantic>=2.13.3",
23
+ ]
24
+ standard=[
25
+ "psutil>=7.2.2",
26
+ "jinja2>=3.1.6",
27
+ ]
28
+ store=[
29
+ "redis>=7.4.0",
30
+ ]
31
+ all=[
32
+ "slowapi>=0.1.9",
33
+ "pydantic>=2.13.3",
34
+ "psutil>=7.2.2",
35
+ "jinja2>=3.1.6",
36
+ "redis>=7.4.0",
37
+ ]
38
+
39
+ [project.scripts]
40
+ utkit = "utkit.cli:main"
41
+
42
+ [build-system]
43
+ requires = ["uv_build >= 0.11.8, <0.12.0"]
44
+ build-backend = "uv_build"
@@ -0,0 +1,6 @@
1
+ def main() -> None:
2
+ print("Hello from utkit!")
3
+
4
+
5
+ #TODO - Add more core utilities and libraries here as needed.
6
+ #TODO : redis, logging,passlib, fastapi utils,sqlalchemy utils, etc.
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+ from slowapi import Limiter, _rate_limit_exceeded_handler as rate_limit_exceeded_handler
3
+ from slowapi.util import get_remote_address
4
+
5
+ __all__ = ["initialize_rate_limiter", "rate_limit_exceeded_handler"]
6
+
7
+
8
+ def initialize_rate_limiter(rate_limit_default: str = "100/minute") -> Limiter:
9
+ return Limiter(key_func=get_remote_address, default_limits=[rate_limit_default])
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+ from slowapi.errors import RateLimitExceeded as RateLimitExceededError
3
+
4
+ __all__ = ["RateLimitExceededError"]
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+ from slowapi.middleware import SlowAPIMiddleware as RateLimitMiddleware
3
+
4
+ __all__ = ["RateLimitMiddleware"]
@@ -0,0 +1,7 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class PaginationParams(BaseModel):
5
+ page: int = Field(1, gt=0 ,description="Page number, starting from 1")
6
+ page_size: int = Field(10, ge=0, description="Number of items per page")
7
+
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from pwdlib import PasswordHash
4
+
5
+ password_hash = PasswordHash.recommended()
6
+
7
+ __all__ = [
8
+ "verify_password","create_password_hash"
9
+ ]
10
+
11
+ def verify_password(plain_password, hashed_password):
12
+ return password_hash.verify(plain_password, hashed_password)
13
+
14
+
15
+ def create_password_hash(password):
16
+ return password_hash.hash(password)
@@ -1,52 +1,52 @@
1
- import subprocess
2
- from pathlib import Path
3
-
4
- import typer
5
- from importlib.metadata import version as get_version
6
-
7
- app = typer.Typer(no_args_is_help=True, help="utkit - core libraries for development.")
8
-
9
-
10
- def _version_callback(value: bool) -> None:
11
- if value:
12
- typer.echo(f"utkit v{get_version('utkit')}")
13
- raise typer.Exit()
14
-
15
-
16
- @app.callback()
17
- def callback(
18
- version: bool = typer.Option(
19
- None,
20
- "--version",
21
- callback=_version_callback,
22
- is_eager=True,
23
- help="Show version and exit.",
24
- ),
25
- ) -> None:
26
- pass
27
-
28
-
29
- @app.command()
30
- def version() -> None:
31
- """Show the current version of utkit."""
32
- typer.echo(f"utkit v{get_version('utkit')}")
33
-
34
-
35
- @app.command()
36
- def docs() -> None:
37
- """Serve the utkit documentation locally on port 8005 and open in browser."""
38
- docs_dir = Path(__file__).parent.parent / "documentation"
39
- if not docs_dir.exists():
40
- typer.echo(f"Documentation folder not found: {docs_dir}", err=True)
41
- raise typer.Exit(code=1)
42
-
43
- typer.echo("Starting docs server at http://localhost:8005 ...")
44
- subprocess.run(
45
- ["zensical", "serve", "--dev-addr", "localhost:8005", "--open"],
46
- cwd=docs_dir,
47
- check=True,
48
- )
49
-
50
-
51
- def main() -> None:
52
- app()
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from importlib.metadata import version as get_version
6
+
7
+ app = typer.Typer(no_args_is_help=True, help="utkit - core libraries for development.")
8
+
9
+
10
+ def _version_callback(value: bool) -> None:
11
+ if value:
12
+ typer.echo(f"utkit v{get_version('utkit')}")
13
+ raise typer.Exit()
14
+
15
+
16
+ @app.callback()
17
+ def callback(
18
+ version: bool = typer.Option(
19
+ None,
20
+ "--version",
21
+ callback=_version_callback,
22
+ is_eager=True,
23
+ help="Show version and exit.",
24
+ ),
25
+ ) -> None:
26
+ pass
27
+
28
+
29
+ @app.command()
30
+ def version() -> None:
31
+ """Show the current version of utkit."""
32
+ typer.echo(f"utkit v{get_version('utkit')}")
33
+
34
+
35
+ @app.command()
36
+ def docs() -> None:
37
+ """Serve the utkit documentation locally on port 8005 and open in browser."""
38
+ docs_dir = Path(__file__).parent.parent / "documentation"
39
+ if not docs_dir.exists():
40
+ typer.echo(f"Documentation folder not found: {docs_dir}", err=True)
41
+ raise typer.Exit(code=1)
42
+
43
+ typer.echo("Starting docs server at http://localhost:8005 ...")
44
+ subprocess.run(
45
+ ["zensical", "serve", "--dev-addr", "localhost:8005", "--open"],
46
+ cwd=docs_dir,
47
+ check=True,
48
+ )
49
+
50
+
51
+ def main() -> None:
52
+ app()
@@ -1,63 +1,63 @@
1
- from __future__ import annotations
2
-
3
- import smtplib
4
- from dataclasses import dataclass, field
5
- from email.mime.multipart import MIMEMultipart
6
- from email.mime.text import MIMEText
7
-
8
-
9
- @dataclass
10
- class SMTPConfig:
11
- """SMTP connection configuration."""
12
-
13
- host: str
14
- port: int
15
- username: str
16
- password: str
17
- use_tls: bool = True
18
-
19
-
20
- @dataclass
21
- class MailMessage:
22
- """Email message parameters."""
23
-
24
- subject: str
25
- from_address: str
26
- to: list[str]
27
- html: str
28
- cc: list[str] = field(default_factory=list)
29
- bcc: list[str] = field(default_factory=list)
30
- reply_to: str | None = None
31
-
32
-
33
- def send_mail(config: SMTPConfig, message: MailMessage) -> None:
34
- """Send an HTML email via SMTP.
35
-
36
- Args:
37
- config: SMTP server connection settings.
38
- message: Email content and addressing details.
39
- """
40
- msg = MIMEMultipart("alternative")
41
- msg["Subject"] = message.subject
42
- msg["From"] = message.from_address
43
- msg["To"] = ", ".join(message.to)
44
-
45
- if message.cc:
46
- msg["Cc"] = ", ".join(message.cc)
47
- if message.reply_to:
48
- msg["Reply-To"] = message.reply_to
49
-
50
- msg.attach(MIMEText(message.html, "html"))
51
-
52
- all_recipients = message.to + message.cc + message.bcc
53
-
54
- if config.use_tls:
55
- with smtplib.SMTP(config.host, config.port) as server:
56
- server.ehlo()
57
- server.starttls()
58
- server.login(config.username, config.password)
59
- server.sendmail(message.from_address, all_recipients, msg.as_string())
60
- else:
61
- with smtplib.SMTP_SSL(config.host, config.port) as server:
62
- server.login(config.username, config.password)
63
- server.sendmail(message.from_address, all_recipients, msg.as_string())
1
+ from __future__ import annotations
2
+
3
+ import smtplib
4
+ from dataclasses import dataclass, field
5
+ from email.mime.multipart import MIMEMultipart
6
+ from email.mime.text import MIMEText
7
+
8
+
9
+ @dataclass
10
+ class SMTPConfig:
11
+ """SMTP connection configuration."""
12
+
13
+ host: str
14
+ port: int
15
+ username: str
16
+ password: str
17
+ use_tls: bool = True
18
+
19
+
20
+ @dataclass
21
+ class MailMessage:
22
+ """Email message parameters."""
23
+
24
+ subject: str
25
+ from_address: str
26
+ to: list[str]
27
+ html: str
28
+ cc: list[str] = field(default_factory=list)
29
+ bcc: list[str] = field(default_factory=list)
30
+ reply_to: str | None = None
31
+
32
+
33
+ def send_mail(config: SMTPConfig, message: MailMessage) -> None:
34
+ """Send an HTML email via SMTP.
35
+
36
+ Args:
37
+ config: SMTP server connection settings.
38
+ message: Email content and addressing details.
39
+ """
40
+ msg = MIMEMultipart("alternative")
41
+ msg["Subject"] = message.subject
42
+ msg["From"] = message.from_address
43
+ msg["To"] = ", ".join(message.to)
44
+
45
+ if message.cc:
46
+ msg["Cc"] = ", ".join(message.cc)
47
+ if message.reply_to:
48
+ msg["Reply-To"] = message.reply_to
49
+
50
+ msg.attach(MIMEText(message.html, "html"))
51
+
52
+ all_recipients = message.to + message.cc + message.bcc
53
+
54
+ if config.use_tls:
55
+ with smtplib.SMTP(config.host, config.port) as server:
56
+ server.ehlo()
57
+ server.starttls()
58
+ server.login(config.username, config.password)
59
+ server.sendmail(message.from_address, all_recipients, msg.as_string())
60
+ else:
61
+ with smtplib.SMTP_SSL(config.host, config.port) as server:
62
+ server.login(config.username, config.password)
63
+ server.sendmail(message.from_address, all_recipients, msg.as_string())
@@ -0,0 +1,88 @@
1
+ import inspect
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import Callable, List
5
+ from loguru import logger
6
+
7
+
8
+ def _add_class_name(record):
9
+ """Patch logger records to include the caller's class name."""
10
+ if record["extra"].get("classname") is not None:
11
+ return
12
+ frame = inspect.currentframe()
13
+ try:
14
+ while frame:
15
+ frame = frame.f_back
16
+ if frame and frame.f_globals.get("__name__") not in (
17
+ "loguru._logger",
18
+ __name__,
19
+ ):
20
+ cls = frame.f_locals.get("self") or frame.f_locals.get("cls")
21
+ if cls is not None:
22
+ record["extra"]["classname"] = (
23
+ cls.__name__ if isinstance(cls, type) else type(cls).__name__
24
+ )
25
+ else:
26
+ record["extra"]["classname"] = "Global"
27
+ break
28
+ else:
29
+ record["extra"]["classname"] = "Global"
30
+ finally:
31
+ del frame
32
+
33
+
34
+ def setup_logging(
35
+ log_base_path="logs",
36
+ patchers: List[Callable] = [],
37
+ rotation: str = "1 day",
38
+ compression: str = "zip",
39
+ level: str = "INFO",
40
+ app_retention: str = "7 days",
41
+ meter_retention: str = "30 days",
42
+ **kwargs,
43
+ ) -> None:
44
+ log_path = Path(log_base_path)
45
+ log_path.mkdir(exist_ok=True)
46
+
47
+ def default_patcher(record):
48
+ for patcher in patchers or []:
49
+ patcher(record)
50
+
51
+ patchers.append(_add_class_name)
52
+ logger.remove()
53
+ logger.configure(patcher=default_patcher)
54
+
55
+ # Common format string (DRY principle)
56
+ log_format = (
57
+ "<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
58
+ "<level>{level: <8}</level> | "
59
+ "<cyan>{name}</cyan>:<cyan>{extra[classname]}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
60
+ "{message} | <blue>Context: {extra}</blue>"
61
+ )
62
+
63
+ log_kwargs = {
64
+ "rotation": rotation,
65
+ "compression": compression,
66
+ "format": log_format,
67
+ "level": level,
68
+ **kwargs,
69
+ }
70
+
71
+ # 2. Console (Everything INFO+)
72
+ logger.add(sys.stderr, format=log_format, level="INFO", colorize=True)
73
+
74
+ # 3. Application Logs (Daily Rotation)
75
+ logger.add(
76
+ log_path / "application_{time:YYYY-MM-DD}.log",
77
+ filter=lambda r: r["extra"].get("type") == "app",
78
+ retention=app_retention,
79
+ **log_kwargs,
80
+ )
81
+
82
+ # 4. Metering Logs
83
+ logger.add(
84
+ log_path / "metering_{time:YYYY-MM-DD}.log",
85
+ filter=lambda r: r["extra"].get("type") == "meter",
86
+ retention=meter_retention,
87
+ **log_kwargs,
88
+ )