platzky 1.3.1__py3-none-any.whl → 1.4.1__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/__init__.py +6 -0
- platzky/admin/admin.py +1 -2
- platzky/blog/blog.py +2 -3
- platzky/config.py +57 -29
- platzky/db/github_json_db.py +1 -7
- platzky/db/google_json_db.py +1 -2
- platzky/db/graph_ql_db.py +4 -12
- platzky/db/json_db.py +10 -16
- platzky/db/json_file_db.py +1 -2
- platzky/db/mongodb_db.py +18 -25
- platzky/debug/__init__.py +9 -0
- platzky/debug/blueprint.py +51 -0
- platzky/{admin → debug}/fake_login.py +15 -20
- platzky/engine.py +19 -4
- platzky/feature_flags.py +141 -0
- platzky/feature_flags_wrapper.py +93 -0
- platzky/platzky.py +10 -17
- platzky/seo/seo.py +5 -6
- {platzky-1.3.1.dist-info → platzky-1.4.1.dist-info}/METADATA +1 -1
- {platzky-1.3.1.dist-info → platzky-1.4.1.dist-info}/RECORD +22 -18
- {platzky-1.3.1.dist-info → platzky-1.4.1.dist-info}/WHEEL +0 -0
- {platzky-1.3.1.dist-info → platzky-1.4.1.dist-info}/licenses/LICENSE +0 -0
platzky/__init__.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
from platzky.engine import Engine as Engine
|
|
2
|
+
from platzky.feature_flags import FakeLogin as FakeLogin
|
|
3
|
+
from platzky.feature_flags import FeatureFlag as FeatureFlag
|
|
4
|
+
from platzky.feature_flags import all_flags as all_flags
|
|
5
|
+
from platzky.feature_flags import build_flag_set as build_flag_set
|
|
6
|
+
from platzky.feature_flags import parse_flags as parse_flags
|
|
7
|
+
from platzky.feature_flags_wrapper import FeatureFlagSet as FeatureFlagSet
|
|
2
8
|
from platzky.platzky import create_app_from_config as create_app_from_config
|
|
3
9
|
from platzky.platzky import create_engine as create_engine
|
platzky/admin/admin.py
CHANGED
|
@@ -20,7 +20,6 @@ def create_admin_blueprint(
|
|
|
20
20
|
Returns:
|
|
21
21
|
Configured Flask Blueprint for admin panel
|
|
22
22
|
"""
|
|
23
|
-
# …rest of the function…
|
|
24
23
|
admin = Blueprint(
|
|
25
24
|
"admin",
|
|
26
25
|
__name__,
|
|
@@ -49,7 +48,7 @@ def create_admin_blueprint(
|
|
|
49
48
|
Returns:
|
|
50
49
|
Rendered login page if not authenticated, admin panel if authenticated
|
|
51
50
|
"""
|
|
52
|
-
user = session.get("user"
|
|
51
|
+
user = session.get("user")
|
|
53
52
|
|
|
54
53
|
if not user:
|
|
55
54
|
return render_template("login.html", login_methods=login_methods)
|
platzky/blog/blog.py
CHANGED
|
@@ -31,11 +31,10 @@ def create_blog_blueprint(db: DB, blog_prefix: str, locale_func: Callable[[], st
|
|
|
31
31
|
Returns:
|
|
32
32
|
Configured Flask Blueprint for blog functionality
|
|
33
33
|
"""
|
|
34
|
-
url_prefix = blog_prefix
|
|
35
34
|
blog = Blueprint(
|
|
36
35
|
"blog",
|
|
37
36
|
__name__,
|
|
38
|
-
url_prefix=
|
|
37
|
+
url_prefix=blog_prefix,
|
|
39
38
|
template_folder=f"{dirname(__file__)}/../templates",
|
|
40
39
|
)
|
|
41
40
|
|
|
@@ -159,7 +158,7 @@ def create_blog_blueprint(db: DB, blog_prefix: str, locale_func: Callable[[], st
|
|
|
159
158
|
Rendered HTML template of the page
|
|
160
159
|
"""
|
|
161
160
|
page = _get_content_or_404(db.get_page, page_slug)
|
|
162
|
-
cover_image_url = page.coverImage.url if page.coverImage
|
|
161
|
+
cover_image_url = (page.coverImage.url or None) if page.coverImage else None
|
|
163
162
|
return render_template("page.html", page=page, cover_image=cover_image_url)
|
|
164
163
|
|
|
165
164
|
@blog.route("/tag/<path:tag>", methods=["GET"])
|
platzky/config.py
CHANGED
|
@@ -9,18 +9,14 @@ import typing as t
|
|
|
9
9
|
import yaml
|
|
10
10
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
11
11
|
|
|
12
|
-
from .attachment.constants import BLOCKED_EXTENSIONS, DEFAULT_MAX_ATTACHMENT_SIZE
|
|
13
|
-
from .db.db import DBConfig
|
|
14
|
-
from .db.db_loader import get_db_module
|
|
12
|
+
from platzky.attachment.constants import BLOCKED_EXTENSIONS, DEFAULT_MAX_ATTACHMENT_SIZE
|
|
13
|
+
from platzky.db.db import DBConfig
|
|
14
|
+
from platzky.db.db_loader import get_db_module
|
|
15
|
+
from platzky.feature_flags import build_flag_set
|
|
16
|
+
from platzky.feature_flags_wrapper import FeatureFlagSet
|
|
15
17
|
|
|
16
18
|
|
|
17
|
-
class
|
|
18
|
-
"""Base model with immutable (frozen) configuration."""
|
|
19
|
-
|
|
20
|
-
model_config = ConfigDict(frozen=True)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class LanguageConfig(StrictBaseModel):
|
|
19
|
+
class LanguageConfig(BaseModel):
|
|
24
20
|
"""Configuration for a single language.
|
|
25
21
|
|
|
26
22
|
Attributes:
|
|
@@ -30,10 +26,12 @@ class LanguageConfig(StrictBaseModel):
|
|
|
30
26
|
domain: Optional domain specific to this language
|
|
31
27
|
"""
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
model_config = ConfigDict(frozen=True)
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
flag: str
|
|
33
|
+
country: str
|
|
34
|
+
domain: t.Optional[str] = None
|
|
37
35
|
|
|
38
36
|
|
|
39
37
|
Languages = dict[str, LanguageConfig]
|
|
@@ -64,7 +62,7 @@ def languages_dict(languages: Languages) -> LanguagesMapping:
|
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
|
|
67
|
-
class TelemetryConfig(
|
|
65
|
+
class TelemetryConfig(BaseModel):
|
|
68
66
|
"""OpenTelemetry configuration for application tracing.
|
|
69
67
|
|
|
70
68
|
Attributes:
|
|
@@ -79,15 +77,17 @@ class TelemetryConfig(StrictBaseModel):
|
|
|
79
77
|
instrument_logging: Enable automatic logging instrumentation (default: True)
|
|
80
78
|
"""
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
model_config = ConfigDict(frozen=True)
|
|
81
|
+
|
|
82
|
+
enabled: bool = False
|
|
83
|
+
endpoint: t.Optional[str] = None
|
|
84
|
+
console_export: bool = False
|
|
85
|
+
timeout: int = Field(default=10, gt=0)
|
|
86
|
+
deployment_environment: t.Optional[str] = None
|
|
87
|
+
service_instance_id: t.Optional[str] = None
|
|
88
|
+
flush_on_request: bool = True
|
|
89
|
+
flush_timeout_ms: int = Field(default=5000, gt=0)
|
|
90
|
+
instrument_logging: bool = True
|
|
91
91
|
|
|
92
92
|
@field_validator("endpoint")
|
|
93
93
|
@classmethod
|
|
@@ -165,7 +165,7 @@ _DEFAULT_ALLOWED_MIME_TYPES: frozenset[str] = frozenset(
|
|
|
165
165
|
)
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
class AttachmentConfig(
|
|
168
|
+
class AttachmentConfig(BaseModel):
|
|
169
169
|
"""Configuration for attachment handling.
|
|
170
170
|
|
|
171
171
|
Attributes:
|
|
@@ -181,6 +181,8 @@ class AttachmentConfig(StrictBaseModel):
|
|
|
181
181
|
Files without extensions are always blocked when allowed_extensions is set.
|
|
182
182
|
"""
|
|
183
183
|
|
|
184
|
+
model_config = ConfigDict(frozen=True)
|
|
185
|
+
|
|
184
186
|
allowed_mime_types: frozenset[str] = Field(default=_DEFAULT_ALLOWED_MIME_TYPES)
|
|
185
187
|
validate_content: bool = Field(default=True)
|
|
186
188
|
allow_unrecognized_content: bool = Field(default=False)
|
|
@@ -220,7 +222,7 @@ class AttachmentConfig(StrictBaseModel):
|
|
|
220
222
|
)
|
|
221
223
|
|
|
222
224
|
|
|
223
|
-
class Config(
|
|
225
|
+
class Config(BaseModel):
|
|
224
226
|
"""Main application configuration.
|
|
225
227
|
|
|
226
228
|
Attributes:
|
|
@@ -239,6 +241,8 @@ class Config(StrictBaseModel):
|
|
|
239
241
|
attachment: Attachment handling configuration
|
|
240
242
|
"""
|
|
241
243
|
|
|
244
|
+
model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
|
|
245
|
+
|
|
242
246
|
app_name: str = Field(alias="APP_NAME")
|
|
243
247
|
secret_key: str = Field(alias="SECRET_KEY")
|
|
244
248
|
db: DBConfig = Field(alias="DB")
|
|
@@ -252,10 +256,25 @@ class Config(StrictBaseModel):
|
|
|
252
256
|
)
|
|
253
257
|
debug: bool = Field(default=False, alias="DEBUG")
|
|
254
258
|
testing: bool = Field(default=False, alias="TESTING")
|
|
255
|
-
feature_flags:
|
|
259
|
+
feature_flags: FeatureFlagSet = Field(
|
|
260
|
+
default_factory=build_flag_set,
|
|
261
|
+
alias="FEATURE_FLAGS",
|
|
262
|
+
)
|
|
256
263
|
telemetry: TelemetryConfig = Field(default_factory=TelemetryConfig, alias="TELEMETRY")
|
|
257
264
|
attachment: AttachmentConfig = Field(default_factory=AttachmentConfig, alias="ATTACHMENT")
|
|
258
265
|
|
|
266
|
+
@field_validator("feature_flags", mode="before")
|
|
267
|
+
@classmethod
|
|
268
|
+
def validate_feature_flags(cls, v: FeatureFlagSet | dict[str, bool] | None) -> FeatureFlagSet:
|
|
269
|
+
"""Coerce dict or None into a FeatureFlagSet."""
|
|
270
|
+
if isinstance(v, FeatureFlagSet):
|
|
271
|
+
return v
|
|
272
|
+
if isinstance(v, dict):
|
|
273
|
+
return build_flag_set(v)
|
|
274
|
+
if v is None:
|
|
275
|
+
return build_flag_set()
|
|
276
|
+
return v
|
|
277
|
+
|
|
259
278
|
@classmethod
|
|
260
279
|
def model_validate(
|
|
261
280
|
cls,
|
|
@@ -267,6 +286,9 @@ class Config(StrictBaseModel):
|
|
|
267
286
|
) -> "Config":
|
|
268
287
|
"""Validate and construct Config from dictionary.
|
|
269
288
|
|
|
289
|
+
Parses the raw FEATURE_FLAGS dict into a frozenset of enabled
|
|
290
|
+
FeatureFlag types via ``parse_flags()``.
|
|
291
|
+
|
|
270
292
|
Args:
|
|
271
293
|
obj: Configuration dictionary
|
|
272
294
|
strict: Enable strict validation
|
|
@@ -276,8 +298,14 @@ class Config(StrictBaseModel):
|
|
|
276
298
|
Returns:
|
|
277
299
|
Validated Config instance
|
|
278
300
|
"""
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
try:
|
|
302
|
+
db_section = obj["DB"]
|
|
303
|
+
db_type = db_section["TYPE"]
|
|
304
|
+
except KeyError as e:
|
|
305
|
+
raise ValueError(f"Missing required config key: {e}. DB.TYPE is required.") from e
|
|
306
|
+
db_cfg_type = get_db_module(db_type).db_config_type()
|
|
307
|
+
obj["DB"] = db_cfg_type.model_validate(db_section)
|
|
308
|
+
|
|
281
309
|
return super().model_validate(
|
|
282
310
|
obj, strict=strict, from_attributes=from_attributes, context=context
|
|
283
311
|
)
|
platzky/db/github_json_db.py
CHANGED
|
@@ -52,13 +52,7 @@ def get_db(config: dict[str, Any]) -> "GithubJsonDb":
|
|
|
52
52
|
Returns:
|
|
53
53
|
Configured GitHub JSON database instance
|
|
54
54
|
"""
|
|
55
|
-
|
|
56
|
-
return GithubJsonDb(
|
|
57
|
-
github_json_db_config.github_token,
|
|
58
|
-
github_json_db_config.repo_name,
|
|
59
|
-
github_json_db_config.branch_name,
|
|
60
|
-
github_json_db_config.path_to_file,
|
|
61
|
-
)
|
|
55
|
+
return db_from_config(GithubJsonDbConfig.model_validate(config))
|
|
62
56
|
|
|
63
57
|
|
|
64
58
|
class GithubJsonDb(JsonDB):
|
platzky/db/google_json_db.py
CHANGED
|
@@ -50,8 +50,7 @@ def get_db(config: dict[str, Any]) -> "GoogleJsonDb":
|
|
|
50
50
|
Returns:
|
|
51
51
|
Configured Google Cloud Storage JSON database instance
|
|
52
52
|
"""
|
|
53
|
-
|
|
54
|
-
return GoogleJsonDb(google_json_db_config.bucket_name, google_json_db_config.source_blob_name)
|
|
53
|
+
return db_from_config(GoogleJsonDbConfig.model_validate(config))
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def get_blob(bucket_name: str, source_blob_name: str) -> "Blob":
|
platzky/db/graph_ql_db.py
CHANGED
|
@@ -29,18 +29,6 @@ class GraphQlDbConfig(DBConfig):
|
|
|
29
29
|
token: str = Field(alias="CMS_TOKEN")
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def get_db(config: GraphQlDbConfig) -> "GraphQL":
|
|
33
|
-
"""Get a GraphQL database instance from configuration.
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
config: GraphQL database configuration
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
Configured GraphQL database instance
|
|
40
|
-
"""
|
|
41
|
-
return GraphQL(config.endpoint, config.token)
|
|
42
|
-
|
|
43
|
-
|
|
44
32
|
def db_from_config(config: GraphQlDbConfig) -> "GraphQL":
|
|
45
33
|
"""Create a GraphQL database instance from configuration.
|
|
46
34
|
|
|
@@ -53,6 +41,10 @@ def db_from_config(config: GraphQlDbConfig) -> "GraphQL":
|
|
|
53
41
|
return GraphQL(config.endpoint, config.token)
|
|
54
42
|
|
|
55
43
|
|
|
44
|
+
# Legacy alias retained for backward compatibility
|
|
45
|
+
get_db = db_from_config
|
|
46
|
+
|
|
47
|
+
|
|
56
48
|
def _standardize_comment(
|
|
57
49
|
comment: dict[str, Any],
|
|
58
50
|
) -> dict[str, Any]:
|
platzky/db/json_db.py
CHANGED
|
@@ -33,8 +33,7 @@ def get_db(config: dict[str, Any]) -> "Json":
|
|
|
33
33
|
Returns:
|
|
34
34
|
Configured JSON database instance
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
return Json(json_db_config.data)
|
|
36
|
+
return db_from_config(JsonDbConfig.model_validate(config))
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def db_from_config(config: JsonDbConfig) -> "Json":
|
|
@@ -129,12 +128,10 @@ class Json(DB):
|
|
|
129
128
|
pages = self._get_site_content().get("pages")
|
|
130
129
|
if pages is None:
|
|
131
130
|
raise ValueError("Pages data is missing")
|
|
132
|
-
|
|
133
|
-
wanted_page = next(list_of_pages, None)
|
|
131
|
+
wanted_page = next((page for page in pages if page["slug"] == slug), None)
|
|
134
132
|
if wanted_page is None:
|
|
135
133
|
raise ValueError(f"Page with slug {slug} not found")
|
|
136
|
-
|
|
137
|
-
return page
|
|
134
|
+
return Page.model_validate(wanted_page)
|
|
138
135
|
|
|
139
136
|
def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
|
|
140
137
|
"""Retrieve menu items for a specific language.
|
|
@@ -146,10 +143,8 @@ class Json(DB):
|
|
|
146
143
|
List of MenuItem objects
|
|
147
144
|
"""
|
|
148
145
|
menu_items_raw = self._get_site_content().get("menu_items", {})
|
|
149
|
-
items_in_lang = menu_items_raw.get(lang,
|
|
150
|
-
|
|
151
|
-
menu_items_list = [MenuItem.model_validate(x) for x in items_in_lang]
|
|
152
|
-
return menu_items_list
|
|
146
|
+
items_in_lang = menu_items_raw.get(lang, [])
|
|
147
|
+
return [MenuItem.model_validate(x) for x in items_in_lang]
|
|
153
148
|
|
|
154
149
|
def get_posts_by_tag(self, tag: str, lang: str) -> list[Post]:
|
|
155
150
|
"""Retrieve posts filtered by tag and language.
|
|
@@ -236,12 +231,11 @@ class Json(DB):
|
|
|
236
231
|
"date": now_utc,
|
|
237
232
|
}
|
|
238
233
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
244
|
-
self._get_site_content()["posts"][post_index]["comments"].append(comment_data)
|
|
234
|
+
posts = self._get_site_content()["posts"]
|
|
235
|
+
post = next((p for p in posts if p["slug"] == post_slug), None)
|
|
236
|
+
if post is None:
|
|
237
|
+
raise ValueError(f"Post with slug {post_slug} not found")
|
|
238
|
+
post["comments"].append(comment_data)
|
|
245
239
|
|
|
246
240
|
def get_plugins_data(self) -> list[dict[str, Any]]:
|
|
247
241
|
"""Retrieve configuration data for all plugins.
|
platzky/db/json_file_db.py
CHANGED
|
@@ -33,8 +33,7 @@ def get_db(config: dict[str, Any]) -> "JsonFile":
|
|
|
33
33
|
Returns:
|
|
34
34
|
Configured JSON file database instance
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
return JsonFile(json_file_db_config.path)
|
|
36
|
+
return db_from_config(JsonFileDbConfig.model_validate(config))
|
|
38
37
|
|
|
39
38
|
|
|
40
39
|
def db_from_config(config: JsonFileDbConfig) -> "JsonFile":
|
platzky/db/mongodb_db.py
CHANGED
|
@@ -37,8 +37,7 @@ def get_db(config: dict[str, Any]) -> "MongoDB":
|
|
|
37
37
|
Returns:
|
|
38
38
|
Configured MongoDB database instance
|
|
39
39
|
"""
|
|
40
|
-
|
|
41
|
-
return MongoDB(mongodb_config.connection_string, mongodb_config.database_name)
|
|
40
|
+
return db_from_config(MongoDbConfig.model_validate(config))
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def db_from_config(config: MongoDbConfig) -> "MongoDB":
|
|
@@ -78,6 +77,10 @@ class MongoDB(DB):
|
|
|
78
77
|
self.menu_items: Collection[Any] = self.db.menu_items
|
|
79
78
|
self.plugins: Collection[Any] = self.db.plugins
|
|
80
79
|
|
|
80
|
+
def _get_site_config(self) -> dict[str, Any] | None:
|
|
81
|
+
"""Retrieve the site configuration document."""
|
|
82
|
+
return self.site_content.find_one({"_id": "config"})
|
|
83
|
+
|
|
81
84
|
def get_app_description(self, lang: str) -> str:
|
|
82
85
|
"""Retrieve the application description for a specific language.
|
|
83
86
|
|
|
@@ -87,9 +90,9 @@ class MongoDB(DB):
|
|
|
87
90
|
Returns:
|
|
88
91
|
Application description text or empty string if not found
|
|
89
92
|
"""
|
|
90
|
-
|
|
91
|
-
if
|
|
92
|
-
return
|
|
93
|
+
site_config = self._get_site_config()
|
|
94
|
+
if site_config and "app_description" in site_config:
|
|
95
|
+
return site_config["app_description"].get(lang, "")
|
|
93
96
|
return ""
|
|
94
97
|
|
|
95
98
|
def get_all_posts(self, lang: str) -> list[Post]:
|
|
@@ -193,10 +196,8 @@ class MongoDB(DB):
|
|
|
193
196
|
Returns:
|
|
194
197
|
Logo image URL or empty string if not found
|
|
195
198
|
"""
|
|
196
|
-
|
|
197
|
-
if
|
|
198
|
-
return site_content.get("logo_url", "")
|
|
199
|
-
return ""
|
|
199
|
+
site_config = self._get_site_config()
|
|
200
|
+
return site_config.get("logo_url", "") if site_config else ""
|
|
200
201
|
|
|
201
202
|
def get_favicon_url(self) -> str:
|
|
202
203
|
"""Retrieve the URL of the application favicon.
|
|
@@ -204,10 +205,8 @@ class MongoDB(DB):
|
|
|
204
205
|
Returns:
|
|
205
206
|
Favicon URL or empty string if not found
|
|
206
207
|
"""
|
|
207
|
-
|
|
208
|
-
if
|
|
209
|
-
return site_content.get("favicon_url", "")
|
|
210
|
-
return ""
|
|
208
|
+
site_config = self._get_site_config()
|
|
209
|
+
return site_config.get("favicon_url", "") if site_config else ""
|
|
211
210
|
|
|
212
211
|
def get_primary_color(self) -> str:
|
|
213
212
|
"""Retrieve the primary color for the application theme.
|
|
@@ -215,10 +214,8 @@ class MongoDB(DB):
|
|
|
215
214
|
Returns:
|
|
216
215
|
Primary color value, defaults to 'white'
|
|
217
216
|
"""
|
|
218
|
-
|
|
219
|
-
if
|
|
220
|
-
return site_content.get("primary_color", "white")
|
|
221
|
-
return "white"
|
|
217
|
+
site_config = self._get_site_config()
|
|
218
|
+
return site_config.get("primary_color", "white") if site_config else "white"
|
|
222
219
|
|
|
223
220
|
def get_secondary_color(self) -> str:
|
|
224
221
|
"""Retrieve the secondary color for the application theme.
|
|
@@ -226,10 +223,8 @@ class MongoDB(DB):
|
|
|
226
223
|
Returns:
|
|
227
224
|
Secondary color value, defaults to 'navy'
|
|
228
225
|
"""
|
|
229
|
-
|
|
230
|
-
if
|
|
231
|
-
return site_content.get("secondary_color", "navy")
|
|
232
|
-
return "navy"
|
|
226
|
+
site_config = self._get_site_config()
|
|
227
|
+
return site_config.get("secondary_color", "navy") if site_config else "navy"
|
|
233
228
|
|
|
234
229
|
def get_plugins_data(self) -> list[dict[str, Any]]:
|
|
235
230
|
"""Retrieve configuration data for all plugins.
|
|
@@ -248,10 +243,8 @@ class MongoDB(DB):
|
|
|
248
243
|
Returns:
|
|
249
244
|
Font name or empty string if not configured
|
|
250
245
|
"""
|
|
251
|
-
|
|
252
|
-
if
|
|
253
|
-
return site_content.get("font", "")
|
|
254
|
-
return ""
|
|
246
|
+
site_config = self._get_site_config()
|
|
247
|
+
return site_config.get("font", "") if site_config else ""
|
|
255
248
|
|
|
256
249
|
def health_check(self) -> None:
|
|
257
250
|
"""Perform a health check on the MongoDB database.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Debug and development utilities.
|
|
2
|
+
|
|
3
|
+
This package contains tools that should only be used in development
|
|
4
|
+
or testing environments, never in production.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from platzky.debug.blueprint import DebugBlueprint, DebugBlueprintProductionError
|
|
8
|
+
|
|
9
|
+
__all__ = ["DebugBlueprint", "DebugBlueprintProductionError"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Debug-only Platzky blueprint."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from flask import Blueprint
|
|
7
|
+
from flask.sansio.app import App
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
_FLASK_DEBUG_TRUTHY = {"1", "true", "yes"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_flask_debug_env() -> bool:
|
|
14
|
+
"""Check if FLASK_DEBUG environment variable indicates debug mode.
|
|
15
|
+
|
|
16
|
+
Flask CLI sets this env var before calling the app factory, so it
|
|
17
|
+
reflects ``--debug`` even though ``app.config["DEBUG"]`` may not yet.
|
|
18
|
+
"""
|
|
19
|
+
return os.environ.get("FLASK_DEBUG", "").lower() in _FLASK_DEBUG_TRUTHY
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DebugBlueprintProductionError(RuntimeError):
|
|
23
|
+
"""Raised when a DebugBlueprint is registered on a production app."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, blueprint_name: str) -> None:
|
|
26
|
+
super().__init__(
|
|
27
|
+
f"SECURITY ERROR: Cannot register DebugBlueprint '{blueprint_name}' in production. "
|
|
28
|
+
f"DEBUG and TESTING are both False. "
|
|
29
|
+
f"Set DEBUG: true or TESTING: true in your config, "
|
|
30
|
+
f"or use flask --debug."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DebugBlueprint(Blueprint):
|
|
35
|
+
"""A Blueprint that can only be registered on apps in debug/testing mode.
|
|
36
|
+
|
|
37
|
+
Raises DebugBlueprintProductionError during registration if the app is not
|
|
38
|
+
in debug or testing mode. This provides a structural guarantee that debug-only
|
|
39
|
+
routes cannot be accidentally enabled in production.
|
|
40
|
+
|
|
41
|
+
Checks ``app.config["DEBUG"]``, ``app.config["TESTING"]``, and the
|
|
42
|
+
``FLASK_DEBUG`` environment variable (set by ``flask --debug`` before
|
|
43
|
+
the app factory is called).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def register(self, app: App, options: dict[str, Any]) -> None:
|
|
48
|
+
"""Register the blueprint, but only if app is in debug/testing mode."""
|
|
49
|
+
if not (app.config.get("DEBUG") or app.config.get("TESTING") or _is_flask_debug_env()):
|
|
50
|
+
raise DebugBlueprintProductionError(self.name)
|
|
51
|
+
super().register(app, options)
|
|
@@ -5,14 +5,15 @@ WARNING: This module provides fake login functionality and should NEVER be used
|
|
|
5
5
|
environments as it bypasses proper authentication and authorization controls.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
import os
|
|
9
8
|
from collections.abc import Callable
|
|
10
9
|
|
|
11
|
-
from flask import
|
|
10
|
+
from flask import flash, redirect, render_template_string, session, url_for
|
|
12
11
|
from flask_wtf import FlaskForm
|
|
13
12
|
from markupsafe import Markup
|
|
14
13
|
from werkzeug.wrappers import Response
|
|
15
14
|
|
|
15
|
+
from platzky.debug.blueprint import DebugBlueprint
|
|
16
|
+
|
|
16
17
|
ROLE_ADMIN = "admin"
|
|
17
18
|
ROLE_NONADMIN = "nonadmin"
|
|
18
19
|
VALID_ROLES = [ROLE_ADMIN, ROLE_NONADMIN]
|
|
@@ -33,8 +34,8 @@ def get_fake_login_html() -> Callable[[], str]:
|
|
|
33
34
|
"""Return a callable that generates HTML for fake login buttons."""
|
|
34
35
|
|
|
35
36
|
def generate_html() -> str:
|
|
36
|
-
admin_url = url_for("
|
|
37
|
-
nonadmin_url = url_for("
|
|
37
|
+
admin_url = url_for("fake_login.handle_fake_login", role="admin")
|
|
38
|
+
nonadmin_url = url_for("fake_login.handle_fake_login", role="nonadmin")
|
|
38
39
|
|
|
39
40
|
# Create a form instance to get the CSRF token
|
|
40
41
|
form = FakeLoginForm()
|
|
@@ -72,24 +73,18 @@ def get_fake_login_html() -> Callable[[], str]:
|
|
|
72
73
|
return generate_html
|
|
73
74
|
|
|
74
75
|
|
|
75
|
-
def
|
|
76
|
-
"""
|
|
76
|
+
def create_fake_login_blueprint() -> DebugBlueprint:
|
|
77
|
+
"""Create a DebugBlueprint with fake login routes.
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"1",
|
|
81
|
-
"true",
|
|
82
|
-
"True",
|
|
83
|
-
True,
|
|
84
|
-
)
|
|
79
|
+
The returned blueprint will raise RuntimeError if registered on an app
|
|
80
|
+
that is not in debug or testing mode.
|
|
85
81
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
82
|
+
Returns:
|
|
83
|
+
DebugBlueprint with fake login routes at /admin/fake-login/<role>.
|
|
84
|
+
"""
|
|
85
|
+
bp = DebugBlueprint("fake_login", __name__, url_prefix="/admin")
|
|
91
86
|
|
|
92
|
-
@
|
|
87
|
+
@bp.route("/fake-login/<role>", methods=["POST"])
|
|
93
88
|
def handle_fake_login(role: str) -> Response:
|
|
94
89
|
form = FakeLoginForm()
|
|
95
90
|
if form.validate_on_submit() and role in VALID_ROLES:
|
|
@@ -102,4 +97,4 @@ def setup_fake_login_routes(admin_blueprint: Blueprint) -> Blueprint:
|
|
|
102
97
|
flash(f"Invalid role: {role}. Must be one of: {', '.join(VALID_ROLES)}", "error")
|
|
103
98
|
return redirect(url_for("admin.admin_panel_home"))
|
|
104
99
|
|
|
105
|
-
return
|
|
100
|
+
return bp
|
platzky/engine.py
CHANGED
|
@@ -13,6 +13,7 @@ from flask_babel import Babel
|
|
|
13
13
|
from platzky.attachment import AttachmentProtocol, create_attachment_class
|
|
14
14
|
from platzky.config import Config
|
|
15
15
|
from platzky.db.db import DB
|
|
16
|
+
from platzky.feature_flags import FeatureFlag
|
|
16
17
|
from platzky.models import CmsModule
|
|
17
18
|
from platzky.notifier import Notifier, NotifierWithAttachments
|
|
18
19
|
|
|
@@ -35,6 +36,7 @@ class Engine(Flask):
|
|
|
35
36
|
"""
|
|
36
37
|
super().__init__(import_name)
|
|
37
38
|
self.config.from_mapping(config.model_dump(by_alias=True))
|
|
39
|
+
self.config["FEATURE_FLAGS"] = config.feature_flags
|
|
38
40
|
self.db = db
|
|
39
41
|
self.Attachment: type[AttachmentProtocol] = create_attachment_class(config.attachment)
|
|
40
42
|
self.notifiers: list[Notifier] = []
|
|
@@ -87,7 +89,7 @@ class Engine(Flask):
|
|
|
87
89
|
"""
|
|
88
90
|
self.notifiers_with_attachments.append(notifier)
|
|
89
91
|
|
|
90
|
-
def add_cms_module(self, module: CmsModule):
|
|
92
|
+
def add_cms_module(self, module: CmsModule) -> None:
|
|
91
93
|
"""Add a CMS module to the modules list."""
|
|
92
94
|
self.cms_modules.append(module)
|
|
93
95
|
|
|
@@ -95,11 +97,11 @@ class Engine(Flask):
|
|
|
95
97
|
def add_login_method(self, login_method: Callable[[], str]) -> None:
|
|
96
98
|
self.login_methods.append(login_method)
|
|
97
99
|
|
|
98
|
-
def add_dynamic_body(self, body: str):
|
|
100
|
+
def add_dynamic_body(self, body: str) -> None:
|
|
99
101
|
self.dynamic_body += body
|
|
100
102
|
|
|
101
|
-
def add_dynamic_head(self,
|
|
102
|
-
self.dynamic_head +=
|
|
103
|
+
def add_dynamic_head(self, head: str) -> None:
|
|
104
|
+
self.dynamic_head += head
|
|
103
105
|
|
|
104
106
|
def get_locale(self) -> str:
|
|
105
107
|
languages = self.config.get("LANGUAGES", {}).keys()
|
|
@@ -113,6 +115,19 @@ class Engine(Flask):
|
|
|
113
115
|
session["language"] = lang
|
|
114
116
|
return lang
|
|
115
117
|
|
|
118
|
+
def is_enabled(self, flag: FeatureFlag) -> bool:
|
|
119
|
+
"""Check whether a feature flag is enabled.
|
|
120
|
+
|
|
121
|
+
This is the primary API for flag checks.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
flag: A FeatureFlag instance.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
True if the flag is enabled.
|
|
128
|
+
"""
|
|
129
|
+
return flag in self.config["FEATURE_FLAGS"]
|
|
130
|
+
|
|
116
131
|
def add_health_check(self, name: str, check_function: Callable[[], None]) -> None:
|
|
117
132
|
"""Register a health check function"""
|
|
118
133
|
if not callable(check_function):
|
platzky/feature_flags.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Feature flags system with instance-based registration.
|
|
2
|
+
|
|
3
|
+
Flags are created as instances of ``FeatureFlag``. Each instance is
|
|
4
|
+
automatically registered and discovered via ``all_flags()``. The primary
|
|
5
|
+
API is ``engine.is_enabled(flag_instance)``.
|
|
6
|
+
|
|
7
|
+
Example::
|
|
8
|
+
|
|
9
|
+
CategoriesHelp = FeatureFlag(alias="CATEGORIES_HELP")
|
|
10
|
+
|
|
11
|
+
# Usage
|
|
12
|
+
app.is_enabled(CategoriesHelp) # True/False
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from platzky.feature_flags_wrapper import FeatureFlagSet
|
|
21
|
+
|
|
22
|
+
_registry: set[FeatureFlag] = set()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FeatureFlag:
|
|
26
|
+
"""A feature flag.
|
|
27
|
+
|
|
28
|
+
Identity is based solely on ``alias``: two flags with the same alias
|
|
29
|
+
are considered equal regardless of ``default`` or ``description``.
|
|
30
|
+
Aliases are expected to be unique across the application.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
alias: The YAML/dict key for this flag.
|
|
34
|
+
default: Whether the flag is enabled by default.
|
|
35
|
+
description: Human-readable description.
|
|
36
|
+
|
|
37
|
+
Example::
|
|
38
|
+
|
|
39
|
+
FakeLogin = FeatureFlag(
|
|
40
|
+
alias="FAKE_LOGIN",
|
|
41
|
+
default=False,
|
|
42
|
+
description="Enable fake login. Never in production.",
|
|
43
|
+
)
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
__slots__ = ("alias", "default", "description", "production_warning")
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
alias: str,
|
|
52
|
+
default: bool = False,
|
|
53
|
+
description: str = "",
|
|
54
|
+
production_warning: bool = False,
|
|
55
|
+
register: bool = True,
|
|
56
|
+
) -> None:
|
|
57
|
+
if not alias:
|
|
58
|
+
raise ValueError("FeatureFlag requires a non-empty 'alias'")
|
|
59
|
+
self.alias = alias
|
|
60
|
+
self.default = default
|
|
61
|
+
self.description = description
|
|
62
|
+
self.production_warning = production_warning
|
|
63
|
+
if register:
|
|
64
|
+
_registry.add(self)
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
return f"FeatureFlag(alias={self.alias!r})"
|
|
68
|
+
|
|
69
|
+
def __hash__(self) -> int:
|
|
70
|
+
return hash(self.alias)
|
|
71
|
+
|
|
72
|
+
def __eq__(self, other: object) -> bool:
|
|
73
|
+
if isinstance(other, FeatureFlag):
|
|
74
|
+
return self.alias == other.alias
|
|
75
|
+
return NotImplemented
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
FakeLogin = FeatureFlag(
|
|
79
|
+
alias="FAKE_LOGIN",
|
|
80
|
+
default=False,
|
|
81
|
+
description="Enable fake login for development. WARNING: Never enable in production.",
|
|
82
|
+
production_warning=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def all_flags() -> frozenset[FeatureFlag]:
|
|
87
|
+
"""Return all registered feature flags.
|
|
88
|
+
|
|
89
|
+
Note: The returned frozenset has no guaranteed iteration order.
|
|
90
|
+
Use ``sorted(all_flags(), key=lambda f: f.alias)`` when
|
|
91
|
+
deterministic ordering is needed (e.g., documentation generation).
|
|
92
|
+
"""
|
|
93
|
+
return frozenset(_registry)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def unregister(flag: FeatureFlag) -> None:
|
|
97
|
+
"""Remove a flag from the registry."""
|
|
98
|
+
_registry.discard(flag)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def clear_registry() -> None:
|
|
102
|
+
"""Remove all flags from the registry. Intended for test isolation."""
|
|
103
|
+
_registry.clear()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def parse_flags(
|
|
107
|
+
raw_data: dict[str, bool] | None = None,
|
|
108
|
+
) -> frozenset[FeatureFlag]:
|
|
109
|
+
"""Build a frozenset of *enabled* flags from raw config data.
|
|
110
|
+
|
|
111
|
+
Uses ``all_flags()`` for discovery. Unknown keys in *raw_data* are
|
|
112
|
+
silently ignored.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
raw_data: Dict of flag alias -> value from config / YAML.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A frozenset containing the enabled FeatureFlag instances.
|
|
119
|
+
"""
|
|
120
|
+
if raw_data is None:
|
|
121
|
+
raw_data = {}
|
|
122
|
+
|
|
123
|
+
return frozenset(flag for flag in all_flags() if raw_data.get(flag.alias, flag.default))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def build_flag_set(raw_data: dict[str, bool] | None = None) -> FeatureFlagSet:
|
|
127
|
+
"""Build a FeatureFlagSet from raw config data.
|
|
128
|
+
|
|
129
|
+
Preserves ALL keys (including unregistered ones) for backward
|
|
130
|
+
compatibility with consumers that use dict-like access.
|
|
131
|
+
"""
|
|
132
|
+
from platzky.feature_flags_wrapper import FeatureFlagSet
|
|
133
|
+
|
|
134
|
+
if raw_data is None:
|
|
135
|
+
raw_data = {}
|
|
136
|
+
|
|
137
|
+
enabled_flags = frozenset(
|
|
138
|
+
flag for flag in all_flags() if raw_data.get(flag.alias, flag.default)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return FeatureFlagSet(enabled_flags, raw_data)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Backward-compatible wrapper for feature flags.
|
|
2
|
+
|
|
3
|
+
.. deprecated:: 1.4.0
|
|
4
|
+
Dict-like access is deprecated. Use typed FeatureFlag instances
|
|
5
|
+
with engine.is_enabled(flag) instead.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
import deprecation
|
|
11
|
+
|
|
12
|
+
from platzky.feature_flags import FeatureFlag
|
|
13
|
+
|
|
14
|
+
_MIGRATION_MSG = (
|
|
15
|
+
"Dict-like access to feature flags is deprecated. "
|
|
16
|
+
"Define a FeatureFlag instance and use engine.is_enabled(flag) instead. "
|
|
17
|
+
"See: https://platzky.readthedocs.io/en/latest/config.html#feature-flags"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_DEPRECATION_WARNING = deprecation.DeprecatedWarning(
|
|
21
|
+
"feature_flags dict access",
|
|
22
|
+
"1.4.0",
|
|
23
|
+
"2.0.0",
|
|
24
|
+
_MIGRATION_MSG,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _warn_dict_access() -> None:
|
|
29
|
+
warnings.warn(_DEPRECATION_WARNING, stacklevel=3)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FeatureFlagSet(dict[str, bool]):
|
|
33
|
+
"""Backward-compatible feature flag collection (dict subclass).
|
|
34
|
+
|
|
35
|
+
.. deprecated:: 1.4.0
|
|
36
|
+
Dict-like access (.get, .KEY, [key]) is deprecated.
|
|
37
|
+
Migrate to typed FeatureFlag + engine.is_enabled().
|
|
38
|
+
|
|
39
|
+
Dual-mode container:
|
|
40
|
+
- ``FeatureFlag in flag_set`` -- typed membership (engine.is_enabled). Intended API.
|
|
41
|
+
- ``flag_set.get("KEY")`` -- deprecated dict access via dict inheritance.
|
|
42
|
+
- ``flag_set.KEY`` -- deprecated Jinja2 attribute access.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_enabled_flags: frozenset[FeatureFlag]
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
enabled_flags: frozenset[FeatureFlag],
|
|
50
|
+
raw_data: dict[str, bool],
|
|
51
|
+
) -> None:
|
|
52
|
+
super().__init__(raw_data)
|
|
53
|
+
# Use object.__setattr__ to bypass __getattr__ override
|
|
54
|
+
object.__setattr__(self, "_enabled_flags", enabled_flags)
|
|
55
|
+
|
|
56
|
+
def __getitem__(self, key: str) -> bool:
|
|
57
|
+
"""Dict bracket access with deprecation warning."""
|
|
58
|
+
_warn_dict_access()
|
|
59
|
+
return super().__getitem__(key)
|
|
60
|
+
|
|
61
|
+
def get(self, key: str, default: bool | None = None) -> bool | None: # type: ignore[override]
|
|
62
|
+
"""Dict .get() access with deprecation warning."""
|
|
63
|
+
_warn_dict_access()
|
|
64
|
+
return super().get(key, default)
|
|
65
|
+
|
|
66
|
+
def _raise_immutable(self, *_args: object, **_kwargs: object) -> None:
|
|
67
|
+
raise TypeError("FeatureFlagSet is immutable")
|
|
68
|
+
|
|
69
|
+
__setitem__ = _raise_immutable
|
|
70
|
+
__delitem__ = _raise_immutable
|
|
71
|
+
pop = _raise_immutable # type: ignore[assignment]
|
|
72
|
+
update = _raise_immutable
|
|
73
|
+
clear = _raise_immutable
|
|
74
|
+
setdefault = _raise_immutable # type: ignore[assignment]
|
|
75
|
+
__ior__ = _raise_immutable # type: ignore[assignment]
|
|
76
|
+
|
|
77
|
+
def __contains__(self, item: object) -> bool:
|
|
78
|
+
"""Support both FeatureFlag membership and string key lookup."""
|
|
79
|
+
if isinstance(item, FeatureFlag):
|
|
80
|
+
return item in self._enabled_flags
|
|
81
|
+
return super().__contains__(item)
|
|
82
|
+
|
|
83
|
+
def __getattr__(self, name: str) -> bool:
|
|
84
|
+
"""Jinja2 dot-notation access: ``{{ feature_flags.SOME_FLAG }}``."""
|
|
85
|
+
try:
|
|
86
|
+
return self[name]
|
|
87
|
+
except KeyError:
|
|
88
|
+
raise AttributeError(f"'{type(self).__name__}' has no attribute {name!r}") from None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def enabled_flags(self) -> frozenset[FeatureFlag]:
|
|
92
|
+
"""The set of enabled typed FeatureFlag instances."""
|
|
93
|
+
return self._enabled_flags
|
platzky/platzky.py
CHANGED
|
@@ -16,6 +16,7 @@ from platzky.config import (
|
|
|
16
16
|
from platzky.db.db import DB
|
|
17
17
|
from platzky.db.db_loader import get_db
|
|
18
18
|
from platzky.engine import Engine
|
|
19
|
+
from platzky.feature_flags import FakeLogin
|
|
19
20
|
from platzky.plugin.plugin_loader import plugify
|
|
20
21
|
from platzky.seo import seo
|
|
21
22
|
from platzky.www_handler import redirect_nonwww_to_www, redirect_www_to_nonwww
|
|
@@ -129,17 +130,6 @@ def create_engine(config: Config, db: DB) -> Engine:
|
|
|
129
130
|
redirect_url = _get_safe_redirect_url(request.referrer, request.host)
|
|
130
131
|
return redirect(redirect_url)
|
|
131
132
|
|
|
132
|
-
def url_link(x: str) -> str:
|
|
133
|
-
"""URL-encode a string for safe use in URLs.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
x: String to encode
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
URL-encoded string with all characters except safe ones escaped
|
|
140
|
-
"""
|
|
141
|
-
return _url_encode(x)
|
|
142
|
-
|
|
143
133
|
@app.context_processor
|
|
144
134
|
def utils() -> dict[str, t.Any]:
|
|
145
135
|
"""Provide utility variables and functions to all templates.
|
|
@@ -150,8 +140,8 @@ def create_engine(config: Config, db: DB) -> Engine:
|
|
|
150
140
|
"""
|
|
151
141
|
locale = app.get_locale()
|
|
152
142
|
lang = config.languages.get(locale)
|
|
153
|
-
flag = lang.flag if lang
|
|
154
|
-
country = lang.country if lang
|
|
143
|
+
flag = lang.flag if lang else ""
|
|
144
|
+
country = lang.country if lang else ""
|
|
155
145
|
return {
|
|
156
146
|
"app_name": config.app_name,
|
|
157
147
|
"app_description": app.db.get_app_description(locale) or config.app_name,
|
|
@@ -159,7 +149,7 @@ def create_engine(config: Config, db: DB) -> Engine:
|
|
|
159
149
|
"current_flag": flag,
|
|
160
150
|
"current_lang_country": country,
|
|
161
151
|
"current_language": locale,
|
|
162
|
-
"url_link":
|
|
152
|
+
"url_link": _url_encode,
|
|
163
153
|
"menu_items": app.db.get_menu_items_in_lang(locale),
|
|
164
154
|
"logo_url": app.db.get_logo_url(),
|
|
165
155
|
"favicon_url": app.db.get_favicon_url(),
|
|
@@ -239,11 +229,14 @@ def create_app_from_config(config: Config) -> Engine:
|
|
|
239
229
|
login_methods=engine.login_methods, cms_modules=engine.cms_modules
|
|
240
230
|
)
|
|
241
231
|
|
|
242
|
-
|
|
243
|
-
|
|
232
|
+
# Two-layer defense: is_enabled() gates the feature flag, and
|
|
233
|
+
# DebugBlueprint.register() independently blocks registration
|
|
234
|
+
# unless the app is in debug or testing mode.
|
|
235
|
+
if engine.is_enabled(FakeLogin):
|
|
236
|
+
from platzky.debug.fake_login import create_fake_login_blueprint, get_fake_login_html
|
|
244
237
|
|
|
245
238
|
engine.login_methods.append(get_fake_login_html())
|
|
246
|
-
|
|
239
|
+
engine.register_blueprint(create_fake_login_blueprint())
|
|
247
240
|
|
|
248
241
|
blog_blueprint = blog.create_blog_blueprint(
|
|
249
242
|
db=engine.db,
|
platzky/seo/seo.py
CHANGED
|
@@ -77,16 +77,15 @@ def create_seo_blueprint(
|
|
|
77
77
|
"""
|
|
78
78
|
lang = locale_func()
|
|
79
79
|
|
|
80
|
-
global url
|
|
81
80
|
host_components = urllib.parse.urlparse(request.host_url)
|
|
82
81
|
host_base = host_components.scheme + "://" + host_components.netloc
|
|
83
82
|
|
|
84
83
|
# Static routes with static content
|
|
85
|
-
static_urls = [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
static_urls = [
|
|
85
|
+
{"loc": f"{host_base}{rule!s}"}
|
|
86
|
+
for rule in current_app.url_map.iter_rules()
|
|
87
|
+
if rule.methods is not None and "GET" in rule.methods and len(rule.arguments) == 0
|
|
88
|
+
]
|
|
90
89
|
|
|
91
90
|
dynamic_urls = get_blog_entries(host_base, lang, db, config["BLOG_PREFIX"])
|
|
92
91
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
platzky/__init__.py,sha256=
|
|
2
|
-
platzky/admin/admin.py,sha256=
|
|
3
|
-
platzky/admin/fake_login.py,sha256=Z_4M4PLQ73qL-sKh05CmDx_nFy8S30PdsNfPPDeFSmE,3528
|
|
1
|
+
platzky/__init__.py,sha256=jn2Ua0ZpKBI3Fb5tFYcRm3kEaR0SytlNBh6guUtO2jg,558
|
|
2
|
+
platzky/admin/admin.py,sha256=_4Q1nId_QGrbSMaAZzl7ziquza3syQxXlBYYXjD_4XM,1660
|
|
4
3
|
platzky/admin/templates/admin.html,sha256=zgjROhSezayZqnNFezvVa0MEfgmXLvOM8HRRaZemkQw,688
|
|
5
4
|
platzky/admin/templates/login.html,sha256=oBNuv130iMTwXrtRnDUDcGIGvu0O2VsIbjQxw-Tjd7Y,380
|
|
6
5
|
platzky/admin/templates/module.html,sha256=WuQZxKQDD4INl-QF2uiKHf9Fmf2h7cEW9RLe1nWKC8k,175
|
|
@@ -9,28 +8,33 @@ platzky/attachment/constants.py,sha256=9WB8w_sKxsq2DM5hfz2ShRI-nUT2E5v9tfBWIOgrq
|
|
|
9
8
|
platzky/attachment/core.py,sha256=-jrelChnHEeowg5d4-41fY6slFfqnv3fEqiI_sMPFd8,9656
|
|
10
9
|
platzky/attachment/mime_validation.py,sha256=VO272nF7K_Mx_Zefuw-10M-Tapj0ZxX197syh8yy0oQ,2874
|
|
11
10
|
platzky/blog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
platzky/blog/blog.py,sha256=
|
|
11
|
+
platzky/blog/blog.py,sha256=UaAS7QpcEsMswMrdAcyoSyuDX1NNQX05ll8D7MKSR3w,5468
|
|
13
12
|
platzky/blog/comment_form.py,sha256=yOuXvX9PZLc6qQLIWZWLFcbwFQD4a849X82PlXKUzdk,805
|
|
14
|
-
platzky/config.py,sha256=
|
|
13
|
+
platzky/config.py,sha256=vldHCB6C_Y9NllkacSJ0-jd3vo3eIw9sG6eDpGQpyOU,11589
|
|
15
14
|
platzky/db/README.md,sha256=IO-LoDsd4dLBZenaz423EZjvEOQu_8m2OC0G7du170w,1753
|
|
16
15
|
platzky/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
16
|
platzky/db/db.py,sha256=gi5uxvY8Ww8O4y2rxaH1Zj_12Yno8SbILvIaWnQPbYQ,4778
|
|
18
17
|
platzky/db/db_loader.py,sha256=YgR16K5Mj5pN0vWYVQxTD4Z6ihG5fZyjbUUCS8LNqJs,999
|
|
19
|
-
platzky/db/github_json_db.py,sha256=
|
|
20
|
-
platzky/db/google_json_db.py,sha256=
|
|
21
|
-
platzky/db/graph_ql_db.py,sha256=
|
|
22
|
-
platzky/db/json_db.py,sha256=
|
|
23
|
-
platzky/db/json_file_db.py,sha256=
|
|
24
|
-
platzky/db/mongodb_db.py,sha256=
|
|
25
|
-
platzky/
|
|
18
|
+
platzky/db/github_json_db.py,sha256=xCh4F569X2sH5WvOVUzH3Nsk0UTXnzVJoxUs5UYbQcs,3110
|
|
19
|
+
platzky/db/google_json_db.py,sha256=e8xh0YxCpVgbWmGshva2CtbLpGukgrz5D2l0vNHEVks,2949
|
|
20
|
+
platzky/db/graph_ql_db.py,sha256=YU61lcJjkJ3kSyVuCQIet1kgX3gbaeNf7Jo0nODBVsI,14295
|
|
21
|
+
platzky/db/json_db.py,sha256=5l2AGw06J57X-BxDaCo5O8ze9uIrBKg7x7JWHe0hecc,7931
|
|
22
|
+
platzky/db/json_file_db.py,sha256=r5LMg-e0bTuu68ayrkZQ93fgIoPpc4RSfwQ48LNieTA,2187
|
|
23
|
+
platzky/db/mongodb_db.py,sha256=vK-fuZUd4LH0AaZjCHDk7YL89_0qmTCginbVL4Rv87A,8427
|
|
24
|
+
platzky/debug/__init__.py,sha256=5VUFjOOJzuXTuCgzQRe9aNjXHoCJSVUerR3RLKCTZC0,301
|
|
25
|
+
platzky/debug/blueprint.py,sha256=_rtrtrOOsj4Pz8AKE_5kLFVedEtipAyN42j-pBVZqnM,1893
|
|
26
|
+
platzky/debug/fake_login.py,sha256=3AmS11Kb4h5gDwWjUY26iDMif0b0x69S97veotnXUp4,3390
|
|
27
|
+
platzky/engine.py,sha256=JeyRS-A0R0fK2BLude_Y4U8G3d-7nRUkuu4OeX86sRI,7400
|
|
28
|
+
platzky/feature_flags.py,sha256=BD-DcPDYfYr2xuMrc61qyw5s6QQBqI1ro_H3-h3vWeo,3947
|
|
29
|
+
platzky/feature_flags_wrapper.py,sha256=NqBoix90VODiILEkO6esRMRKsKZQVHgOa3VUWV3PPmU,3138
|
|
26
30
|
platzky/locale/en/LC_MESSAGES/messages.po,sha256=WaZGlFAegKRq7CSz69dWKic-mKvQFhVvssvExxNmGaU,1400
|
|
27
31
|
platzky/locale/pl/LC_MESSAGES/messages.po,sha256=sUPxMKDeEOoZ5UIg94rGxZD06YVWiAMWIby2XE51Hrc,1624
|
|
28
32
|
platzky/models.py,sha256=Ws5ZSWf5EhcpFxl3Yeze2pQiesjnjAA_haBJ90bN6lk,7435
|
|
29
33
|
platzky/notifier.py,sha256=fh6_sdeD9TRFPkBjnuSz9jH6UKoSDkaqqGfZD1dmVZc,1214
|
|
30
|
-
platzky/platzky.py,sha256=
|
|
34
|
+
platzky/platzky.py,sha256=cQ9X-eczMs0Bhcve0432nDC8_H5UvXCgk5dJLQLqcDo,9214
|
|
31
35
|
platzky/plugin/plugin.py,sha256=KZb6VEph__lx9xrv5Ay4h4XkFFYbodV5OimaG6B9IDc,2812
|
|
32
36
|
platzky/plugin/plugin_loader.py,sha256=eKG6zodUCkiRLxJ2ZX9zdN4-ZrZ9EwssoY1SDtThaFo,6707
|
|
33
|
-
platzky/seo/seo.py,sha256=
|
|
37
|
+
platzky/seo/seo.py,sha256=9ZSvT7zepHFmmrOSsCvaiSiO0B25E7bdgrm1Ctby5fE,3796
|
|
34
38
|
platzky/static/blog.css,sha256=TrppzgQbj4UtuTufDCdblyNTVAqgIbhD66Cziyv_xnY,7893
|
|
35
39
|
platzky/static/styles.css,sha256=wLUug7kd3S3aXylkq6eDKiGEeHD7CfP0RsDBZnahlhc,3184
|
|
36
40
|
platzky/telemetry.py,sha256=iXYvEt0Uw5Hx8lAxyr45dpQ_SiE2NxmJkoSx-JSRJyM,5011
|
|
@@ -46,7 +50,7 @@ platzky/templates/post.html,sha256=GSgjIZsOQKtNx3cEbquSjZ5L4whPnG6MzRyoq9k4B8Q,1
|
|
|
46
50
|
platzky/templates/robots.txt,sha256=2_j2tiYtYJnzZUrANiX9pvBxyw5Dp27fR_co18BPEJ0,116
|
|
47
51
|
platzky/templates/sitemap.xml,sha256=iIJZ91_B5ZuNLCHsRtsGKZlBAXojOTP8kffqKLacgvs,578
|
|
48
52
|
platzky/www_handler.py,sha256=pF6Rmvem1sdVqHD7z3RLrDuG-CwAqfGCti50_NPsB2w,725
|
|
49
|
-
platzky-1.
|
|
50
|
-
platzky-1.
|
|
51
|
-
platzky-1.
|
|
52
|
-
platzky-1.
|
|
53
|
+
platzky-1.4.1.dist-info/METADATA,sha256=7sZy3c0h0DRh_uEVwK1t_hqrV8XLaWarDbpLj2SA4xM,2595
|
|
54
|
+
platzky-1.4.1.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
55
|
+
platzky-1.4.1.dist-info/licenses/LICENSE,sha256=wCdfk-qEosi6BDwiBulMfKMi0hxp1UXV0DdjLrRm788,1077
|
|
56
|
+
platzky-1.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|