platzky 0.4.3__tar.gz → 1.0.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.
Files changed (48) hide show
  1. platzky-1.0.1/LICENSE +21 -0
  2. {platzky-0.4.3 → platzky-1.0.1}/PKG-INFO +13 -1
  3. platzky-1.0.1/platzky/config.py +209 -0
  4. {platzky-0.4.3 → platzky-1.0.1}/platzky/engine.py +1 -0
  5. {platzky-0.4.3 → platzky-1.0.1}/platzky/platzky.py +20 -0
  6. platzky-1.0.1/platzky/telemetry.py +132 -0
  7. {platzky-0.4.3 → platzky-1.0.1}/pyproject.toml +42 -1
  8. platzky-0.4.3/platzky/config.py +0 -69
  9. {platzky-0.4.3 → platzky-1.0.1}/README.md +0 -0
  10. {platzky-0.4.3 → platzky-1.0.1}/platzky/__init__.py +0 -0
  11. {platzky-0.4.3 → platzky-1.0.1}/platzky/admin/admin.py +0 -0
  12. {platzky-0.4.3 → platzky-1.0.1}/platzky/admin/fake_login.py +0 -0
  13. {platzky-0.4.3 → platzky-1.0.1}/platzky/admin/templates/admin.html +0 -0
  14. {platzky-0.4.3 → platzky-1.0.1}/platzky/admin/templates/login.html +0 -0
  15. {platzky-0.4.3 → platzky-1.0.1}/platzky/admin/templates/module.html +0 -0
  16. {platzky-0.4.3 → platzky-1.0.1}/platzky/blog/__init__.py +0 -0
  17. {platzky-0.4.3 → platzky-1.0.1}/platzky/blog/blog.py +0 -0
  18. {platzky-0.4.3 → platzky-1.0.1}/platzky/blog/comment_form.py +0 -0
  19. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/README.md +0 -0
  20. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/__init__.py +0 -0
  21. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/db.py +0 -0
  22. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/db_loader.py +0 -0
  23. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/github_json_db.py +0 -0
  24. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/google_json_db.py +0 -0
  25. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/graph_ql_db.py +0 -0
  26. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/json_db.py +0 -0
  27. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/json_file_db.py +0 -0
  28. {platzky-0.4.3 → platzky-1.0.1}/platzky/db/mongodb_db.py +0 -0
  29. {platzky-0.4.3 → platzky-1.0.1}/platzky/locale/en/LC_MESSAGES/messages.po +0 -0
  30. {platzky-0.4.3 → platzky-1.0.1}/platzky/locale/pl/LC_MESSAGES/messages.po +0 -0
  31. {platzky-0.4.3 → platzky-1.0.1}/platzky/models.py +0 -0
  32. {platzky-0.4.3 → platzky-1.0.1}/platzky/plugin/plugin.py +0 -0
  33. {platzky-0.4.3 → platzky-1.0.1}/platzky/plugin/plugin_loader.py +0 -0
  34. {platzky-0.4.3 → platzky-1.0.1}/platzky/seo/seo.py +0 -0
  35. {platzky-0.4.3 → platzky-1.0.1}/platzky/static/blog.css +0 -0
  36. {platzky-0.4.3 → platzky-1.0.1}/platzky/static/styles.css +0 -0
  37. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/404.html +0 -0
  38. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/base.html +0 -0
  39. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/blog.html +0 -0
  40. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/body_meta.html +0 -0
  41. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/dynamic_css.html +0 -0
  42. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/feed.xml +0 -0
  43. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/head_meta.html +0 -0
  44. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/page.html +0 -0
  45. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/post.html +0 -0
  46. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/robots.txt +0 -0
  47. {platzky-0.4.3 → platzky-1.0.1}/platzky/templates/sitemap.xml +0 -0
  48. {platzky-0.4.3 → platzky-1.0.1}/platzky/www_handler.py +0 -0
platzky-1.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Platzky Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: platzky
3
- Version: 0.4.3
3
+ Version: 1.0.1
4
4
  Summary: Not only blog engine
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Requires-Python: >=3.10,<4.0
7
8
  Classifier: License :: OSI Approved :: MIT License
8
9
  Classifier: Programming Language :: Python :: 3
@@ -11,6 +12,8 @@ Classifier: Programming Language :: Python :: 3.11
11
12
  Classifier: Programming Language :: Python :: 3.12
12
13
  Classifier: Programming Language :: Python :: 3.13
13
14
  Classifier: Programming Language :: Python :: 3.14
15
+ Provides-Extra: docs
16
+ Provides-Extra: telemetry
14
17
  Requires-Dist: Flask (==3.0.3)
15
18
  Requires-Dist: Flask-Babel (>=4.0.0,<5.0.0)
16
19
  Requires-Dist: Flask-Minify (>=0.42,<0.43)
@@ -21,9 +24,18 @@ Requires-Dist: deprecation (>=2.1.0,<3.0.0)
21
24
  Requires-Dist: google-cloud-storage (>=2.5.0,<3.0.0)
22
25
  Requires-Dist: gql (>=3.4.0,<4.0.0)
23
26
  Requires-Dist: humanize (>=4.9.0,<5.0.0)
27
+ Requires-Dist: myst-parser (>=4.0.0,<5.0.0) ; extra == "docs"
28
+ Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0) ; extra == "telemetry"
29
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc (>=1.27.0,<2.0.0) ; extra == "telemetry"
30
+ Requires-Dist: opentelemetry-instrumentation-flask (>=0.48b0,<0.49) ; extra == "telemetry"
31
+ Requires-Dist: opentelemetry-instrumentation-logging (>=0.48b0,<0.49) ; extra == "telemetry"
32
+ Requires-Dist: opentelemetry-sdk (>=1.27.0,<2.0.0) ; extra == "telemetry"
24
33
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
25
34
  Requires-Dist: pygithub (>=2.6.1,<3.0.0)
26
35
  Requires-Dist: pymongo (>=4.7.0,<5.0.0)
36
+ Requires-Dist: sphinx (>=8.0.0,<9.0.0) ; extra == "docs"
37
+ Requires-Dist: sphinx-rtd-theme (>=3.0.0,<4.0.0) ; extra == "docs"
38
+ Requires-Dist: tomli (>=2.0.0,<3.0.0) ; extra == "docs"
27
39
  Description-Content-Type: text/markdown
28
40
 
29
41
  ![Github Actions](https://github.com/platzky/platzky/actions/workflows/tests.yml/badge.svg?event=push&branch=main)
@@ -0,0 +1,209 @@
1
+ """Configuration module for Platzky application.
2
+
3
+ This module defines all configuration models and parsing logic for the application.
4
+ """
5
+
6
+ import sys
7
+ import typing as t
8
+
9
+ import yaml
10
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
11
+
12
+ from .db.db import DBConfig
13
+ from .db.db_loader import get_db_module
14
+
15
+
16
+ class StrictBaseModel(BaseModel):
17
+ """Base model with immutable (frozen) configuration."""
18
+
19
+ model_config = ConfigDict(frozen=True)
20
+
21
+
22
+ class LanguageConfig(StrictBaseModel):
23
+ """Configuration for a single language.
24
+
25
+ Attributes:
26
+ name: Display name of the language
27
+ flag: Flag icon code (country code)
28
+ country: Country code
29
+ domain: Optional domain specific to this language
30
+ """
31
+
32
+ name: str = Field(alias="name")
33
+ flag: str = Field(alias="flag")
34
+ country: str = Field(alias="country")
35
+ domain: t.Optional[str] = Field(default=None, alias="domain")
36
+
37
+
38
+ Languages = dict[str, LanguageConfig]
39
+ LanguagesMapping = t.Mapping[str, t.Mapping[str, str]]
40
+
41
+ # Validation error messages
42
+ _INVALID_ENDPOINT_FORMAT_MSG = (
43
+ "Invalid endpoint: '{}'. Must be host:port or [http|https]://host[:port]"
44
+ )
45
+ _INVALID_ENDPOINT_SCHEME_MSG = "Invalid endpoint scheme: '{}'. Must be http or https"
46
+ _MISSING_HOSTNAME_MSG = "Invalid endpoint: '{}'. Missing hostname"
47
+
48
+
49
+ def languages_dict(languages: Languages) -> LanguagesMapping:
50
+ """Convert Languages configuration to a mapping dictionary.
51
+
52
+ Excludes None values to align with type signature.
53
+
54
+ Args:
55
+ languages: Dictionary of language configurations
56
+
57
+ Returns:
58
+ Mapping of language codes to their configuration dictionaries (excludes None values)
59
+ """
60
+ return {
61
+ name: {k: v for k, v in lang.model_dump().items() if v is not None}
62
+ for name, lang in languages.items()
63
+ }
64
+
65
+
66
+ class TelemetryConfig(StrictBaseModel):
67
+ """OpenTelemetry configuration for application tracing.
68
+
69
+ Attributes:
70
+ enabled: Enable or disable telemetry tracing
71
+ endpoint: OTLP gRPC endpoint (e.g., localhost:4317 or http://localhost:4317)
72
+ console_export: Export traces to console for debugging
73
+ timeout: Timeout in seconds for exporter (default: 10)
74
+ deployment_environment: Deployment environment (e.g., production, staging, dev)
75
+ service_instance_id: Service instance ID (auto-generated if not provided)
76
+ flush_on_request: Flush spans after each request (default: True, may impact latency)
77
+ flush_timeout_ms: Timeout in milliseconds for per-request flush (default: 5000)
78
+ instrument_logging: Enable automatic logging instrumentation (default: True)
79
+ """
80
+
81
+ enabled: bool = Field(default=False, alias="enabled")
82
+ endpoint: t.Optional[str] = Field(default=None, alias="endpoint")
83
+ console_export: bool = Field(default=False, alias="console_export")
84
+ timeout: int = Field(default=10, alias="timeout", gt=0)
85
+ deployment_environment: t.Optional[str] = Field(default=None, alias="deployment_environment")
86
+ service_instance_id: t.Optional[str] = Field(default=None, alias="service_instance_id")
87
+ flush_on_request: bool = Field(default=True, alias="flush_on_request")
88
+ flush_timeout_ms: int = Field(default=5000, alias="flush_timeout_ms", gt=0)
89
+ instrument_logging: bool = Field(default=True, alias="instrument_logging")
90
+
91
+ @field_validator("endpoint")
92
+ @classmethod
93
+ def validate_endpoint(cls, v: t.Optional[str]) -> t.Optional[str]:
94
+ """Validate endpoint URL format.
95
+
96
+ Accepts OTLP/gRPC spec-compliant formats:
97
+ - host:port (e.g., localhost:4317)
98
+ - http://host[:port]
99
+ - https://host[:port]
100
+
101
+ Note: grpc:// scheme is NOT supported per OTLP spec and will be rejected.
102
+ """
103
+ if v is None:
104
+ return v
105
+
106
+ from urllib.parse import urlparse
107
+
108
+ # Check if it has a scheme (contains ://)
109
+ if "://" not in v:
110
+ # Must be host:port format - validate it has a colon
111
+ if ":" in v and not v.startswith("/"):
112
+ return v
113
+ raise ValueError(_INVALID_ENDPOINT_FORMAT_MSG.format(v))
114
+
115
+ # Parse URL with scheme
116
+ parsed = urlparse(v)
117
+
118
+ # Validate scheme (only http/https per OTLP spec, grpc is NOT supported)
119
+ if parsed.scheme not in ("http", "https"):
120
+ raise ValueError(_INVALID_ENDPOINT_SCHEME_MSG.format(parsed.scheme))
121
+
122
+ # Validate hostname exists
123
+ if not parsed.hostname:
124
+ raise ValueError(_MISSING_HOSTNAME_MSG.format(v))
125
+
126
+ return v
127
+
128
+
129
+ class Config(StrictBaseModel):
130
+ """Main application configuration.
131
+
132
+ Attributes:
133
+ app_name: Application name
134
+ secret_key: Flask secret key for sessions
135
+ db: Database configuration
136
+ use_www: Redirect non-www to www URLs
137
+ seo_prefix: URL prefix for SEO routes
138
+ blog_prefix: URL prefix for blog routes
139
+ languages: Supported languages configuration
140
+ domain_to_lang: Domain to language mapping
141
+ translation_directories: Additional translation directories
142
+ debug: Enable debug mode
143
+ testing: Enable testing mode
144
+ feature_flags: Feature flag configuration
145
+ telemetry: OpenTelemetry configuration
146
+ """
147
+
148
+ app_name: str = Field(alias="APP_NAME")
149
+ secret_key: str = Field(alias="SECRET_KEY")
150
+ db: DBConfig = Field(alias="DB")
151
+ use_www: bool = Field(default=True, alias="USE_WWW")
152
+ seo_prefix: str = Field(default="/", alias="SEO_PREFIX")
153
+ blog_prefix: str = Field(default="/", alias="BLOG_PREFIX")
154
+ languages: Languages = Field(default_factory=dict, alias="LANGUAGES")
155
+ domain_to_lang: dict[str, str] = Field(default_factory=dict, alias="DOMAIN_TO_LANG")
156
+ translation_directories: list[str] = Field(
157
+ default_factory=list,
158
+ alias="TRANSLATION_DIRECTORIES",
159
+ )
160
+ debug: bool = Field(default=False, alias="DEBUG")
161
+ testing: bool = Field(default=False, alias="TESTING")
162
+ feature_flags: t.Optional[dict[str, bool]] = Field(default=None, alias="FEATURE_FLAGS")
163
+ telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig, alias="TELEMETRY")
164
+
165
+ @classmethod
166
+ def model_validate(
167
+ cls,
168
+ obj: t.Any,
169
+ *,
170
+ strict: bool | None = None,
171
+ from_attributes: bool | None = None,
172
+ context: dict[str, t.Any] | None = None,
173
+ ) -> "Config":
174
+ """Validate and construct Config from dictionary.
175
+
176
+ Args:
177
+ obj: Configuration dictionary
178
+ strict: Enable strict validation
179
+ from_attributes: Populate from object attributes
180
+ context: Additional validation context
181
+
182
+ Returns:
183
+ Validated Config instance
184
+ """
185
+ db_cfg_type = get_db_module(obj["DB"]["TYPE"]).db_config_type()
186
+ obj["DB"] = db_cfg_type.model_validate(obj["DB"])
187
+ return super().model_validate(
188
+ obj, strict=strict, from_attributes=from_attributes, context=context
189
+ )
190
+
191
+ @classmethod
192
+ def parse_yaml(cls, path: str) -> "Config":
193
+ """Parse configuration from YAML file.
194
+
195
+ Args:
196
+ path: Path to YAML configuration file
197
+
198
+ Returns:
199
+ Validated Config instance
200
+
201
+ Raises:
202
+ SystemExit: If config file is not found
203
+ """
204
+ try:
205
+ with open(path, "r") as f:
206
+ return cls.model_validate(yaml.safe_load(f))
207
+ except FileNotFoundError:
208
+ print(f"Config file not found: {path}", file=sys.stderr)
209
+ raise SystemExit(1)
@@ -19,6 +19,7 @@ class Engine(Flask):
19
19
  self.dynamic_body = ""
20
20
  self.dynamic_head = ""
21
21
  self.health_checks: List[Tuple[str, Callable[[], None]]] = []
22
+ self.telemetry_instrumented: bool = False
22
23
  directory = os.path.dirname(os.path.realpath(__file__))
23
24
  locale_dir = os.path.join(directory, "locale")
24
25
  config.translation_directories.append(locale_dir)
@@ -17,6 +17,12 @@ from platzky.plugin.plugin_loader import plugify
17
17
  from platzky.seo import seo
18
18
  from platzky.www_handler import redirect_nonwww_to_www, redirect_www_to_nonwww
19
19
 
20
+ _MISSING_OTEL_MSG = (
21
+ "OpenTelemetry is not installed. Install with: "
22
+ "poetry add opentelemetry-api opentelemetry-sdk "
23
+ "opentelemetry-instrumentation-flask opentelemetry-exporter-otlp-proto-grpc"
24
+ )
25
+
20
26
 
21
27
  def create_engine(config: Config, db) -> Engine:
22
28
  app = Engine(config, db, __name__)
@@ -85,6 +91,20 @@ def create_app_from_config(config: Config) -> Engine:
85
91
  db = get_db(config.db)
86
92
  engine = create_engine(config, db)
87
93
 
94
+ # Setup telemetry (optional feature)
95
+ if config.telemetry.enabled:
96
+ try:
97
+ from platzky.telemetry import setup_telemetry
98
+
99
+ setup_telemetry(engine, config.telemetry)
100
+ except ImportError as e:
101
+ raise ImportError(_MISSING_OTEL_MSG) from e
102
+ except ValueError as e:
103
+ raise ValueError(
104
+ f"Telemetry configuration error: {e}. "
105
+ "Check your telemetry settings in the configuration file."
106
+ ) from e
107
+
88
108
  admin_blueprint = admin.create_admin_blueprint(
89
109
  login_methods=engine.login_methods, cms_modules=engine.cms_modules
90
110
  )
@@ -0,0 +1,132 @@
1
+ import atexit
2
+ import socket
3
+ import uuid
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from platzky.config import TelemetryConfig
7
+
8
+ if TYPE_CHECKING:
9
+ from opentelemetry.trace import Tracer
10
+
11
+ from platzky.engine import Engine
12
+
13
+ # Error messages
14
+ _MISSING_EXPORTERS_MSG = (
15
+ "Telemetry is enabled but no exporters are configured. "
16
+ "Set endpoint or console_export=True to export traces."
17
+ )
18
+
19
+
20
+ def setup_telemetry(app: "Engine", telemetry_config: TelemetryConfig) -> Optional["Tracer"]:
21
+ """Setup OpenTelemetry tracing for Flask application.
22
+
23
+ Configures and initializes OpenTelemetry tracing with OTLP and/or console exporters.
24
+ Automatically instruments Flask to capture HTTP requests and trace information.
25
+ Optionally instruments logging to add trace context to log records.
26
+
27
+ Args:
28
+ app: Engine instance (Flask-based application)
29
+ telemetry_config: Telemetry configuration specifying endpoint and export options
30
+
31
+ Returns:
32
+ OpenTelemetry tracer instance if enabled, None otherwise
33
+
34
+ Raises:
35
+ ImportError: If OpenTelemetry packages are not installed when telemetry is enabled
36
+ ValueError: If telemetry is enabled but no exporters are configured
37
+ """
38
+ if not telemetry_config.enabled:
39
+ return None
40
+
41
+ # Reject telemetry enabled without exporters (creates overhead without benefit)
42
+ if not telemetry_config.endpoint and not telemetry_config.console_export:
43
+ raise ValueError(_MISSING_EXPORTERS_MSG)
44
+
45
+ # If already instrumented, return tracer without rebuilding provider/exporters
46
+ if app.telemetry_instrumented:
47
+ from opentelemetry import trace
48
+
49
+ return trace.get_tracer(__name__)
50
+
51
+ # Import OpenTelemetry modules (will raise ImportError if not installed)
52
+ from opentelemetry import trace
53
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
54
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
55
+ from opentelemetry.instrumentation.logging import LoggingInstrumentor
56
+ from opentelemetry.sdk.resources import Resource
57
+ from opentelemetry.sdk.trace import TracerProvider
58
+ from opentelemetry.sdk.trace.export import (
59
+ BatchSpanProcessor,
60
+ ConsoleSpanExporter,
61
+ SimpleSpanProcessor,
62
+ )
63
+ from opentelemetry.semconv.resource import ResourceAttributes
64
+
65
+ # Build resource attributes
66
+ service_name = app.config.get("APP_NAME", "platzky")
67
+ resource_attrs: dict[str, str] = {
68
+ ResourceAttributes.SERVICE_NAME: service_name,
69
+ }
70
+
71
+ # Auto-detect service version from package metadata
72
+ from importlib.metadata import PackageNotFoundError
73
+ from importlib.metadata import version as get_version
74
+
75
+ try:
76
+ resource_attrs[ResourceAttributes.SERVICE_VERSION] = get_version("platzky")
77
+ except PackageNotFoundError:
78
+ pass # Version not available
79
+
80
+ if telemetry_config.deployment_environment:
81
+ resource_attrs[ResourceAttributes.DEPLOYMENT_ENVIRONMENT] = (
82
+ telemetry_config.deployment_environment
83
+ )
84
+
85
+ # Add instance ID (user-provided or auto-generated)
86
+ if telemetry_config.service_instance_id:
87
+ resource_attrs[ResourceAttributes.SERVICE_INSTANCE_ID] = (
88
+ telemetry_config.service_instance_id
89
+ )
90
+ else:
91
+ # Generate unique instance ID: hostname + short UUID
92
+ hostname = socket.gethostname()
93
+ instance_uuid = str(uuid.uuid4())[:8]
94
+ resource_attrs[ResourceAttributes.SERVICE_INSTANCE_ID] = f"{hostname}-{instance_uuid}"
95
+
96
+ resource = Resource.create(resource_attrs)
97
+ provider = TracerProvider(resource=resource)
98
+
99
+ # Configure exporter based on endpoint
100
+ if telemetry_config.endpoint:
101
+ exporter = OTLPSpanExporter(
102
+ endpoint=telemetry_config.endpoint, timeout=telemetry_config.timeout
103
+ )
104
+ provider.add_span_processor(BatchSpanProcessor(exporter))
105
+
106
+ # Optional console export
107
+ if telemetry_config.console_export:
108
+ provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
109
+
110
+ trace.set_tracer_provider(provider)
111
+ FlaskInstrumentor().instrument_app(app)
112
+
113
+ # Instrument logging to add trace context to log records
114
+ # Note: set_logging_format=False to avoid modifying existing log formats
115
+ # Users can access trace context in their custom formatters via log record attributes
116
+ if telemetry_config.instrument_logging:
117
+ LoggingInstrumentor().instrument(set_logging_format=False)
118
+
119
+ app.telemetry_instrumented = True
120
+
121
+ # Optionally flush spans after each request (may impact latency)
122
+ if telemetry_config.flush_on_request:
123
+
124
+ @app.teardown_appcontext
125
+ def flush_telemetry(_exc: Optional[BaseException] = None) -> None:
126
+ """Flush pending spans after request completion."""
127
+ provider.force_flush(timeout_millis=telemetry_config.flush_timeout_ms)
128
+
129
+ # Shutdown provider once at process exit
130
+ atexit.register(provider.shutdown)
131
+
132
+ return trace.get_tracer(__name__)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "platzky"
3
- version = "0.4.3"
3
+ version = "1.0.1"
4
4
  description = "Not only blog engine"
5
5
  authors = []
6
6
  license = "MIT"
@@ -21,6 +21,30 @@ pydantic = "^2.7.1"
21
21
  deprecation = "^2.1.0"
22
22
  pygithub = "^2.6.1"
23
23
  pymongo = "^4.7.0"
24
+ sphinx = {version = "^8.0.0", optional = true}
25
+ sphinx-rtd-theme = {version = "^3.0.0", optional = true}
26
+ tomli = {version = "^2.0.0", optional = true}
27
+ myst-parser = {version = "^4.0.0", optional = true}
28
+ opentelemetry-api = {version = "^1.27.0", optional = true}
29
+ opentelemetry-sdk = {version = "^1.27.0", optional = true}
30
+ opentelemetry-instrumentation-flask = {version = "^0.48b0", optional = true}
31
+ opentelemetry-instrumentation-logging = {version = "^0.48b0", optional = true}
32
+ opentelemetry-exporter-otlp-proto-grpc = {version = "^1.27.0", optional = true}
33
+
34
+ [tool.poetry.extras]
35
+ docs = [
36
+ "sphinx",
37
+ "sphinx-rtd-theme",
38
+ "myst-parser",
39
+ "tomli"
40
+ ]
41
+ telemetry = [
42
+ "opentelemetry-api",
43
+ "opentelemetry-sdk",
44
+ "opentelemetry-instrumentation-flask",
45
+ "opentelemetry-instrumentation-logging",
46
+ "opentelemetry-exporter-otlp-proto-grpc"
47
+ ]
24
48
 
25
49
  [tool.poetry.group.dev.dependencies]
26
50
  pytest = "^8.2.1"
@@ -32,6 +56,12 @@ ruff = "^0.4.4"
32
56
  pyright = "^1.1.364"
33
57
  beautifulsoup4 = "^4.12.3"
34
58
  python-semantic-release = "^9.8.0"
59
+ interrogate = "^1.7.0"
60
+ opentelemetry-api = "^1.27.0"
61
+ opentelemetry-sdk = "^1.27.0"
62
+ opentelemetry-instrumentation-flask = "^0.48b0"
63
+ opentelemetry-instrumentation-logging = "^0.48b0"
64
+ opentelemetry-exporter-otlp-proto-grpc = "^1.27.0"
35
65
 
36
66
  [build-system]
37
67
  requires = ["poetry-core"]
@@ -48,6 +78,8 @@ exclude_lines = [
48
78
  "@abstractmethod",
49
79
  "@abc.abstractmethod"
50
80
  ]
81
+ fail_under = 70
82
+ show_missing = true
51
83
 
52
84
  [tool.pyright]
53
85
  pythonVersion = "3.10"
@@ -113,3 +145,12 @@ type = "github"
113
145
 
114
146
  [tool.semantic_release.remote.token]
115
147
  env = "GH_TOKEN"
148
+
149
+ [tool.interrogate]
150
+ fail-under = 20
151
+ ignore-init-method = true
152
+ ignore-init-module = true
153
+ ignore-magic = true
154
+ ignore-private = true
155
+ ignore-semiprivate = false
156
+ exclude = ["tests", "docs"]
@@ -1,69 +0,0 @@
1
- import sys
2
- import typing as t
3
-
4
- import yaml
5
- from pydantic import BaseModel, ConfigDict, Field
6
-
7
- from .db.db import DBConfig
8
- from .db.db_loader import get_db_module
9
-
10
-
11
- class StrictBaseModel(BaseModel):
12
- model_config = ConfigDict(frozen=True)
13
-
14
-
15
- class LanguageConfig(StrictBaseModel):
16
- name: str = Field(alias="name")
17
- flag: str = Field(alias="flag")
18
- country: str = Field(alias="country")
19
- domain: t.Optional[str] = Field(default=None, alias="domain")
20
-
21
-
22
- Languages = dict[str, LanguageConfig]
23
- LanguagesMapping = t.Mapping[str, t.Mapping[str, str]]
24
-
25
-
26
- def languages_dict(languages: Languages) -> LanguagesMapping:
27
- return {name: lang.model_dump() for name, lang in languages.items()}
28
-
29
-
30
- class Config(StrictBaseModel):
31
- app_name: str = Field(alias="APP_NAME")
32
- secret_key: str = Field(alias="SECRET_KEY")
33
- db: DBConfig = Field(alias="DB")
34
- use_www: bool = Field(default=True, alias="USE_WWW")
35
- seo_prefix: str = Field(default="/", alias="SEO_PREFIX")
36
- blog_prefix: str = Field(default="/", alias="BLOG_PREFIX")
37
- languages: Languages = Field(default_factory=dict, alias="LANGUAGES")
38
- domain_to_lang: dict[str, str] = Field(default_factory=dict, alias="DOMAIN_TO_LANG")
39
- translation_directories: list[str] = Field(
40
- default_factory=list,
41
- alias="TRANSLATION_DIRECTORIES",
42
- )
43
- debug: bool = Field(default=False, alias="DEBUG")
44
- testing: bool = Field(default=False, alias="TESTING")
45
- feature_flags: t.Optional[dict[str, bool]] = Field(default_factory=dict, alias="FEATURE_FLAGS")
46
-
47
- @classmethod
48
- def model_validate(
49
- cls,
50
- obj: t.Any,
51
- *,
52
- strict: bool | None = None,
53
- from_attributes: bool | None = None,
54
- context: dict[str, t.Any] | None = None,
55
- ) -> "Config":
56
- db_cfg_type = get_db_module(obj["DB"]["TYPE"]).db_config_type()
57
- obj["DB"] = db_cfg_type.model_validate(obj["DB"])
58
- return super().model_validate(
59
- obj, strict=strict, from_attributes=from_attributes, context=context
60
- )
61
-
62
- @classmethod
63
- def parse_yaml(cls, path: str) -> "Config":
64
- try:
65
- with open(path, "r") as f:
66
- return cls.model_validate(yaml.safe_load(f))
67
- except FileNotFoundError:
68
- print(f"Config file not found: {path}", file=sys.stderr)
69
- raise SystemExit(1)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes