platzky 0.4.3__py3-none-any.whl → 1.0.0__py3-none-any.whl

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.
platzky/config.py CHANGED
@@ -1,18 +1,34 @@
1
+ """Configuration module for Platzky application.
2
+
3
+ This module defines all configuration models and parsing logic for the application.
4
+ """
5
+
1
6
  import sys
2
7
  import typing as t
3
8
 
4
9
  import yaml
5
- from pydantic import BaseModel, ConfigDict, Field
10
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
6
11
 
7
12
  from .db.db import DBConfig
8
13
  from .db.db_loader import get_db_module
9
14
 
10
15
 
11
16
  class StrictBaseModel(BaseModel):
17
+ """Base model with immutable (frozen) configuration."""
18
+
12
19
  model_config = ConfigDict(frozen=True)
13
20
 
14
21
 
15
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
+
16
32
  name: str = Field(alias="name")
17
33
  flag: str = Field(alias="flag")
18
34
  country: str = Field(alias="country")
@@ -22,12 +38,111 @@ class LanguageConfig(StrictBaseModel):
22
38
  Languages = dict[str, LanguageConfig]
23
39
  LanguagesMapping = t.Mapping[str, t.Mapping[str, str]]
24
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
+
25
48
 
26
49
  def languages_dict(languages: Languages) -> LanguagesMapping:
27
- return {name: lang.model_dump() for name, lang in languages.items()}
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
+ """
79
+
80
+ enabled: bool = Field(default=False, alias="enabled")
81
+ endpoint: t.Optional[str] = Field(default=None, alias="endpoint")
82
+ console_export: bool = Field(default=False, alias="console_export")
83
+ timeout: int = Field(default=10, alias="timeout", gt=0)
84
+ deployment_environment: t.Optional[str] = Field(default=None, alias="deployment_environment")
85
+ service_instance_id: t.Optional[str] = Field(default=None, alias="service_instance_id")
86
+ flush_on_request: bool = Field(default=True, alias="flush_on_request")
87
+ flush_timeout_ms: int = Field(default=5000, alias="flush_timeout_ms", gt=0)
88
+
89
+ @field_validator("endpoint")
90
+ @classmethod
91
+ def validate_endpoint(cls, v: t.Optional[str]) -> t.Optional[str]:
92
+ """Validate endpoint URL format.
93
+
94
+ Accepts OTLP/gRPC spec-compliant formats:
95
+ - host:port (e.g., localhost:4317)
96
+ - http://host[:port]
97
+ - https://host[:port]
98
+
99
+ Note: grpc:// scheme is NOT supported per OTLP spec and will be rejected.
100
+ """
101
+ if v is None:
102
+ return v
103
+
104
+ from urllib.parse import urlparse
105
+
106
+ # Check if it has a scheme (contains ://)
107
+ if "://" not in v:
108
+ # Must be host:port format - validate it has a colon
109
+ if ":" in v and not v.startswith("/"):
110
+ return v
111
+ raise ValueError(_INVALID_ENDPOINT_FORMAT_MSG.format(v))
112
+
113
+ # Parse URL with scheme
114
+ parsed = urlparse(v)
115
+
116
+ # Validate scheme (only http/https per OTLP spec, grpc is NOT supported)
117
+ if parsed.scheme not in ("http", "https"):
118
+ raise ValueError(_INVALID_ENDPOINT_SCHEME_MSG.format(parsed.scheme))
119
+
120
+ # Validate hostname exists
121
+ if not parsed.hostname:
122
+ raise ValueError(_MISSING_HOSTNAME_MSG.format(v))
123
+
124
+ return v
28
125
 
29
126
 
30
127
  class Config(StrictBaseModel):
128
+ """Main application configuration.
129
+
130
+ Attributes:
131
+ app_name: Application name
132
+ secret_key: Flask secret key for sessions
133
+ db: Database configuration
134
+ use_www: Redirect non-www to www URLs
135
+ seo_prefix: URL prefix for SEO routes
136
+ blog_prefix: URL prefix for blog routes
137
+ languages: Supported languages configuration
138
+ domain_to_lang: Domain to language mapping
139
+ translation_directories: Additional translation directories
140
+ debug: Enable debug mode
141
+ testing: Enable testing mode
142
+ feature_flags: Feature flag configuration
143
+ telemetry: OpenTelemetry configuration
144
+ """
145
+
31
146
  app_name: str = Field(alias="APP_NAME")
32
147
  secret_key: str = Field(alias="SECRET_KEY")
33
148
  db: DBConfig = Field(alias="DB")
@@ -42,7 +157,8 @@ class Config(StrictBaseModel):
42
157
  )
43
158
  debug: bool = Field(default=False, alias="DEBUG")
44
159
  testing: bool = Field(default=False, alias="TESTING")
45
- feature_flags: t.Optional[dict[str, bool]] = Field(default_factory=dict, alias="FEATURE_FLAGS")
160
+ feature_flags: t.Optional[dict[str, bool]] = Field(default=None, alias="FEATURE_FLAGS")
161
+ telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig, alias="TELEMETRY")
46
162
 
47
163
  @classmethod
48
164
  def model_validate(
@@ -53,6 +169,17 @@ class Config(StrictBaseModel):
53
169
  from_attributes: bool | None = None,
54
170
  context: dict[str, t.Any] | None = None,
55
171
  ) -> "Config":
172
+ """Validate and construct Config from dictionary.
173
+
174
+ Args:
175
+ obj: Configuration dictionary
176
+ strict: Enable strict validation
177
+ from_attributes: Populate from object attributes
178
+ context: Additional validation context
179
+
180
+ Returns:
181
+ Validated Config instance
182
+ """
56
183
  db_cfg_type = get_db_module(obj["DB"]["TYPE"]).db_config_type()
57
184
  obj["DB"] = db_cfg_type.model_validate(obj["DB"])
58
185
  return super().model_validate(
@@ -61,6 +188,17 @@ class Config(StrictBaseModel):
61
188
 
62
189
  @classmethod
63
190
  def parse_yaml(cls, path: str) -> "Config":
191
+ """Parse configuration from YAML file.
192
+
193
+ Args:
194
+ path: Path to YAML configuration file
195
+
196
+ Returns:
197
+ Validated Config instance
198
+
199
+ Raises:
200
+ SystemExit: If config file is not found
201
+ """
64
202
  try:
65
203
  with open(path, "r") as f:
66
204
  return cls.model_validate(yaml.safe_load(f))
platzky/engine.py CHANGED
@@ -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)
platzky/platzky.py CHANGED
@@ -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
  )
platzky/telemetry.py ADDED
@@ -0,0 +1,123 @@
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
+
26
+ Args:
27
+ app: Engine instance (Flask-based application)
28
+ telemetry_config: Telemetry configuration specifying endpoint and export options
29
+
30
+ Returns:
31
+ OpenTelemetry tracer instance if enabled, None otherwise
32
+
33
+ Raises:
34
+ ImportError: If OpenTelemetry packages are not installed when telemetry is enabled
35
+ ValueError: If telemetry is enabled but no exporters are configured
36
+ """
37
+ if not telemetry_config.enabled:
38
+ return None
39
+
40
+ # Reject telemetry enabled without exporters (creates overhead without benefit)
41
+ if not telemetry_config.endpoint and not telemetry_config.console_export:
42
+ raise ValueError(_MISSING_EXPORTERS_MSG)
43
+
44
+ # If already instrumented, return tracer without rebuilding provider/exporters
45
+ if app.telemetry_instrumented:
46
+ from opentelemetry import trace
47
+
48
+ return trace.get_tracer(__name__)
49
+
50
+ # Import OpenTelemetry modules (will raise ImportError if not installed)
51
+ from opentelemetry import trace
52
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
53
+ from opentelemetry.instrumentation.flask import FlaskInstrumentor
54
+ from opentelemetry.sdk.resources import Resource
55
+ from opentelemetry.sdk.trace import TracerProvider
56
+ from opentelemetry.sdk.trace.export import (
57
+ BatchSpanProcessor,
58
+ ConsoleSpanExporter,
59
+ SimpleSpanProcessor,
60
+ )
61
+ from opentelemetry.semconv.resource import ResourceAttributes
62
+
63
+ # Build resource attributes
64
+ service_name = app.config.get("APP_NAME", "platzky")
65
+ resource_attrs: dict[str, str] = {
66
+ ResourceAttributes.SERVICE_NAME: service_name,
67
+ }
68
+
69
+ # Auto-detect service version from package metadata
70
+ from importlib.metadata import PackageNotFoundError
71
+ from importlib.metadata import version as get_version
72
+
73
+ try:
74
+ resource_attrs[ResourceAttributes.SERVICE_VERSION] = get_version("platzky")
75
+ except PackageNotFoundError:
76
+ pass # Version not available
77
+
78
+ if telemetry_config.deployment_environment:
79
+ resource_attrs[ResourceAttributes.DEPLOYMENT_ENVIRONMENT] = (
80
+ telemetry_config.deployment_environment
81
+ )
82
+
83
+ # Add instance ID (user-provided or auto-generated)
84
+ if telemetry_config.service_instance_id:
85
+ resource_attrs[ResourceAttributes.SERVICE_INSTANCE_ID] = (
86
+ telemetry_config.service_instance_id
87
+ )
88
+ else:
89
+ # Generate unique instance ID: hostname + short UUID
90
+ hostname = socket.gethostname()
91
+ instance_uuid = str(uuid.uuid4())[:8]
92
+ resource_attrs[ResourceAttributes.SERVICE_INSTANCE_ID] = f"{hostname}-{instance_uuid}"
93
+
94
+ resource = Resource.create(resource_attrs)
95
+ provider = TracerProvider(resource=resource)
96
+
97
+ # Configure exporter based on endpoint
98
+ if telemetry_config.endpoint:
99
+ exporter = OTLPSpanExporter(
100
+ endpoint=telemetry_config.endpoint, timeout=telemetry_config.timeout
101
+ )
102
+ provider.add_span_processor(BatchSpanProcessor(exporter))
103
+
104
+ # Optional console export
105
+ if telemetry_config.console_export:
106
+ provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))
107
+
108
+ trace.set_tracer_provider(provider)
109
+ FlaskInstrumentor().instrument_app(app)
110
+ app.telemetry_instrumented = True
111
+
112
+ # Optionally flush spans after each request (may impact latency)
113
+ if telemetry_config.flush_on_request:
114
+
115
+ @app.teardown_appcontext
116
+ def flush_telemetry(_exc: Optional[BaseException] = None) -> None:
117
+ """Flush pending spans after request completion."""
118
+ provider.force_flush(timeout_millis=telemetry_config.flush_timeout_ms)
119
+
120
+ # Shutdown provider once at process exit
121
+ atexit.register(provider.shutdown)
122
+
123
+ return trace.get_tracer(__name__)
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: platzky
3
- Version: 0.4.3
3
+ Version: 1.0.0
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,17 @@ 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-sdk (>=1.27.0,<2.0.0) ; extra == "telemetry"
24
32
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
25
33
  Requires-Dist: pygithub (>=2.6.1,<3.0.0)
26
34
  Requires-Dist: pymongo (>=4.7.0,<5.0.0)
35
+ Requires-Dist: sphinx (>=8.0.0,<9.0.0) ; extra == "docs"
36
+ Requires-Dist: sphinx-rtd-theme (>=3.0.0,<4.0.0) ; extra == "docs"
37
+ Requires-Dist: tomli (>=2.0.0,<3.0.0) ; extra == "docs"
27
38
  Description-Content-Type: text/markdown
28
39
 
29
40
  ![Github Actions](https://github.com/platzky/platzky/actions/workflows/tests.yml/badge.svg?event=push&branch=main)
@@ -7,7 +7,7 @@ platzky/admin/templates/module.html,sha256=WuQZxKQDD4INl-QF2uiKHf9Fmf2h7cEW9RLe1
7
7
  platzky/blog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  platzky/blog/blog.py,sha256=L9IYWxnLo1v1h_wLF-0HyG1Y4RSGg7maEMnYxhTgG5Y,2971
9
9
  platzky/blog/comment_form.py,sha256=4lkNJ_S_2DZmJBbz-NPDqahvy2Zz5AGNH2spFeGIop4,513
10
- platzky/config.py,sha256=M3gmZI9yI-ThgmTA4RKsAPcnJwJjcWhXipYzq3hO-Hk,2346
10
+ platzky/config.py,sha256=N5cQjV8Jh_fETw9jXE-UOcC-bSlC3l_04yMMtRwXKXw,7365
11
11
  platzky/db/README.md,sha256=IO-LoDsd4dLBZenaz423EZjvEOQu_8m2OC0G7du170w,1753
12
12
  platzky/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  platzky/db/db.py,sha256=0h5rGCBO_N1wBqJRl5EoiW_bFDpNIvmNwuA0hJi89jw,3060
@@ -18,16 +18,17 @@ platzky/db/graph_ql_db.py,sha256=af6yy1R27YO8N9zJWU7VgU7optRgpdk_1ZUtab_1eT4,896
18
18
  platzky/db/json_db.py,sha256=NUBPy4jt-y37TYq4SCGaSgief3MbBWL_Efw8Bxp8Jo0,4046
19
19
  platzky/db/json_file_db.py,sha256=tPo92n5zG7vGpunn5vl66zISHBziQdxBttitvc5hPug,1030
20
20
  platzky/db/mongodb_db.py,sha256=28KO8XmTEiqE7FcNBzw_pfxOy6Vo-T7qsHdUlh59QX0,5174
21
- platzky/engine.py,sha256=Kv242PsB8lVz_FCYdGogd8o5zGmn5Msev3B3lfYRUXA,5411
21
+ platzky/engine.py,sha256=0bxKzfK83ic-VNlKcDut_84_u5EmY2baU5JcJsleUoM,5461
22
22
  platzky/locale/en/LC_MESSAGES/messages.po,sha256=WaZGlFAegKRq7CSz69dWKic-mKvQFhVvssvExxNmGaU,1400
23
23
  platzky/locale/pl/LC_MESSAGES/messages.po,sha256=sUPxMKDeEOoZ5UIg94rGxZD06YVWiAMWIby2XE51Hrc,1624
24
24
  platzky/models.py,sha256=DZZgKW2Q3fY2GMdikFUmAgpsRqT5VKAOwP6RmEsmO2M,1871
25
- platzky/platzky.py,sha256=oWI-R2lZzpmmqrRqhswssZO1Z14R8fAdRFJCbyRTyeY,3868
25
+ platzky/platzky.py,sha256=8mTqdYqRKONv2oGvQF5Y0fuOomH7ZFYvXtxc7ZohBLE,4585
26
26
  platzky/plugin/plugin.py,sha256=tV8aobIzMDJe1frKUAi4kLbrTAIS0FWE3oYpktSo6Ug,1633
27
27
  platzky/plugin/plugin_loader.py,sha256=MeQ8LNbrOomwXgc1ISHuyhjZd2mzYKen70eDShWs-Co,3497
28
28
  platzky/seo/seo.py,sha256=N_MmAA4KJZmmrDUh0hYNtD8ycOwpNKow4gVSAv8V3N4,2631
29
29
  platzky/static/blog.css,sha256=TrppzgQbj4UtuTufDCdblyNTVAqgIbhD66Cziyv_xnY,7893
30
30
  platzky/static/styles.css,sha256=U5ddGIK-VcGRJZ3BdOpMp0pR__k6rNEMsuQXkP4tFQ0,686
31
+ platzky/telemetry.py,sha256=NQuueK-uwZDdnpWaOTFSMvzRWWg6Yu4jPOMPYdMidNY,4524
31
32
  platzky/templates/404.html,sha256=EheoLSWylOscLH8FmcMA4c6Jw14i5HkSvE_GXzGIrUo,78
32
33
  platzky/templates/base.html,sha256=clvWlVOxNLqSQxBpPao3qnKKzkU2q48Apf1WbHJgYfE,4003
33
34
  platzky/templates/blog.html,sha256=aPl-DzLX85bHv7tN8UjlABR086PUJ9IGlGbIBioFHGA,1281
@@ -40,6 +41,7 @@ platzky/templates/post.html,sha256=GSgjIZsOQKtNx3cEbquSjZ5L4whPnG6MzRyoq9k4B8Q,1
40
41
  platzky/templates/robots.txt,sha256=2_j2tiYtYJnzZUrANiX9pvBxyw5Dp27fR_co18BPEJ0,116
41
42
  platzky/templates/sitemap.xml,sha256=iIJZ91_B5ZuNLCHsRtsGKZlBAXojOTP8kffqKLacgvs,578
42
43
  platzky/www_handler.py,sha256=pF6Rmvem1sdVqHD7z3RLrDuG-CwAqfGCti50_NPsB2w,725
43
- platzky-0.4.3.dist-info/METADATA,sha256=RMfCt6cM7vEEWaS_lmahJZ9kNyy53c41_gmu064X6tU,1818
44
- platzky-0.4.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
45
- platzky-0.4.3.dist-info/RECORD,,
44
+ platzky-1.0.0.dist-info/METADATA,sha256=NSQ3OIyrJ4NQsdu3QRIo-c4jQQ9mM0HoqwmQK2EPvKc,2463
45
+ platzky-1.0.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
46
+ platzky-1.0.0.dist-info/licenses/LICENSE,sha256=wCdfk-qEosi6BDwiBulMfKMi0hxp1UXV0DdjLrRm788,1077
47
+ platzky-1.0.0.dist-info/RECORD,,
@@ -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.