platzky 1.0.1__py3-none-any.whl → 1.2.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/admin/admin.py +25 -3
- platzky/admin/fake_login.py +3 -2
- platzky/blog/blog.py +126 -37
- platzky/blog/comment_form.py +10 -0
- platzky/config.py +1 -1
- platzky/db/db.py +61 -10
- platzky/db/db_loader.py +5 -2
- platzky/db/github_json_db.py +42 -4
- platzky/db/google_json_db.py +63 -7
- platzky/db/graph_ql_db.py +216 -31
- platzky/db/json_db.py +172 -38
- platzky/db/json_file_db.py +46 -6
- platzky/db/mongodb_db.py +134 -6
- platzky/engine.py +9 -7
- platzky/models.py +160 -24
- platzky/platzky.py +169 -23
- platzky/plugin/plugin.py +39 -3
- platzky/plugin/plugin_loader.py +108 -12
- platzky/seo/seo.py +51 -16
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/METADATA +1 -1
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/RECORD +23 -23
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/WHEEL +0 -0
- {platzky-1.0.1.dist-info → platzky-1.2.0.dist-info}/licenses/LICENSE +0 -0
platzky/platzky.py
CHANGED
|
@@ -4,6 +4,8 @@ import urllib.parse
|
|
|
4
4
|
from flask import redirect, render_template, request, session
|
|
5
5
|
from flask_minify import Minify
|
|
6
6
|
from flask_wtf import CSRFProtect
|
|
7
|
+
from werkzeug.exceptions import HTTPException
|
|
8
|
+
from werkzeug.wrappers import Response
|
|
7
9
|
|
|
8
10
|
from platzky.admin import admin
|
|
9
11
|
from platzky.blog import blog
|
|
@@ -11,6 +13,7 @@ from platzky.config import (
|
|
|
11
13
|
Config,
|
|
12
14
|
languages_dict,
|
|
13
15
|
)
|
|
16
|
+
from platzky.db.db import DB
|
|
14
17
|
from platzky.db.db_loader import get_db
|
|
15
18
|
from platzky.engine import Engine
|
|
16
19
|
from platzky.plugin.plugin_loader import plugify
|
|
@@ -24,38 +27,131 @@ _MISSING_OTEL_MSG = (
|
|
|
24
27
|
)
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def _url_encode(x: str) -> str:
|
|
31
|
+
"""URL-encode a string for safe use in URLs.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
x: String to encode
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
URL-encoded string with all characters except safe ones escaped
|
|
38
|
+
"""
|
|
39
|
+
return urllib.parse.quote(x, safe="")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_language_domain(config: Config, lang: str) -> t.Optional[str]:
|
|
43
|
+
"""Get the domain associated with a language.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
config: Application configuration
|
|
47
|
+
lang: Language code to look up
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Domain string if language has a dedicated domain, None otherwise
|
|
51
|
+
"""
|
|
52
|
+
lang_cfg = config.languages.get(lang)
|
|
53
|
+
if lang_cfg is None:
|
|
54
|
+
return None
|
|
55
|
+
return lang_cfg.domain
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _get_safe_redirect_url(referrer: t.Optional[str], current_host: str) -> str:
|
|
59
|
+
"""Get a safe redirect URL by validating the referrer.
|
|
60
|
+
|
|
61
|
+
Prevents open redirect vulnerabilities by only allowing same-host redirects.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
referrer: The HTTP referrer header value
|
|
65
|
+
current_host: The current request host
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The referrer URL if safe, otherwise "/"
|
|
69
|
+
"""
|
|
70
|
+
if not referrer:
|
|
71
|
+
return "/"
|
|
72
|
+
|
|
73
|
+
referrer_parsed = urllib.parse.urlparse(referrer)
|
|
74
|
+
# Only redirect to referrer if it's from the same host
|
|
75
|
+
if referrer_parsed.netloc == current_host:
|
|
76
|
+
return referrer
|
|
77
|
+
return "/"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def create_engine(config: Config, db: DB) -> Engine:
|
|
81
|
+
"""Create and configure a Platzky Engine instance.
|
|
82
|
+
|
|
83
|
+
Sets up the core application with database connection, request handlers,
|
|
84
|
+
route definitions, and context processors for template rendering.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
config: Application configuration object
|
|
88
|
+
db: Database instance for data persistence
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Configured Engine instance with plugins loaded
|
|
92
|
+
"""
|
|
28
93
|
app = Engine(config, db, __name__)
|
|
29
94
|
|
|
30
95
|
@app.before_request
|
|
31
|
-
def handle_www_redirection():
|
|
96
|
+
def handle_www_redirection() -> t.Optional[Response]:
|
|
97
|
+
"""Handle WWW subdomain redirection based on configuration.
|
|
98
|
+
|
|
99
|
+
Redirects requests to/from www subdomain based on config.use_www setting.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Redirect response if redirection is needed, None otherwise
|
|
103
|
+
"""
|
|
32
104
|
if config.use_www:
|
|
33
105
|
return redirect_nonwww_to_www()
|
|
34
|
-
|
|
35
|
-
return redirect_www_to_nonwww()
|
|
36
|
-
|
|
37
|
-
def get_langs_domain(lang: str) -> t.Optional[str]:
|
|
38
|
-
lang_cfg = config.languages.get(lang)
|
|
39
|
-
if lang_cfg is None:
|
|
40
|
-
return None
|
|
41
|
-
return lang_cfg.domain
|
|
106
|
+
return redirect_www_to_nonwww()
|
|
42
107
|
|
|
43
108
|
@app.route("/lang/<string:lang>", methods=["GET"])
|
|
44
|
-
def change_language(lang):
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
109
|
+
def change_language(lang: str) -> Response | tuple[str, int]:
|
|
110
|
+
"""Change the user's language preference.
|
|
111
|
+
|
|
112
|
+
If the language has a dedicated domain, redirects to that domain.
|
|
113
|
+
Otherwise, sets the language in the session and returns to the referrer.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
lang: Language code to switch to
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Redirect response to the language domain or referrer page, or 404 if invalid
|
|
120
|
+
"""
|
|
121
|
+
# Only allow configured languages
|
|
122
|
+
if lang not in config.languages:
|
|
123
|
+
return render_template("404.html", title="404"), 404
|
|
124
|
+
|
|
125
|
+
if new_domain := _get_language_domain(config, lang):
|
|
126
|
+
return redirect(f"{request.scheme}://{new_domain}", code=302)
|
|
127
|
+
|
|
128
|
+
session["language"] = lang
|
|
129
|
+
redirect_url = _get_safe_redirect_url(request.referrer, request.host)
|
|
130
|
+
return redirect(redirect_url)
|
|
50
131
|
|
|
51
132
|
def url_link(x: str) -> str:
|
|
52
|
-
|
|
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)
|
|
53
142
|
|
|
54
143
|
@app.context_processor
|
|
55
|
-
def utils():
|
|
144
|
+
def utils() -> dict[str, t.Any]:
|
|
145
|
+
"""Provide utility variables and functions to all templates.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Dictionary of template context variables including app metadata,
|
|
149
|
+
language settings, styling configuration, and helper functions
|
|
150
|
+
"""
|
|
56
151
|
locale = app.get_locale()
|
|
57
|
-
|
|
58
|
-
|
|
152
|
+
lang = config.languages.get(locale)
|
|
153
|
+
flag = lang.flag if lang is not None else ""
|
|
154
|
+
country = lang.country if lang is not None else ""
|
|
59
155
|
return {
|
|
60
156
|
"app_name": config.app_name,
|
|
61
157
|
"app_description": app.db.get_app_description(locale) or config.app_name,
|
|
@@ -73,21 +169,55 @@ def create_engine(config: Config, db) -> Engine:
|
|
|
73
169
|
}
|
|
74
170
|
|
|
75
171
|
@app.context_processor
|
|
76
|
-
def dynamic_body():
|
|
172
|
+
def dynamic_body() -> dict[str, str]:
|
|
173
|
+
"""Provide dynamic body content to all templates.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Dictionary with dynamic_body content for injection into page body
|
|
177
|
+
"""
|
|
77
178
|
return {"dynamic_body": app.dynamic_body}
|
|
78
179
|
|
|
79
180
|
@app.context_processor
|
|
80
|
-
def dynamic_head():
|
|
181
|
+
def dynamic_head() -> dict[str, str]:
|
|
182
|
+
"""Provide dynamic head content to all templates.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Dictionary with dynamic_head content for injection into page head
|
|
186
|
+
"""
|
|
81
187
|
return {"dynamic_head": app.dynamic_head}
|
|
82
188
|
|
|
83
189
|
@app.errorhandler(404)
|
|
84
|
-
def page_not_found(
|
|
190
|
+
def page_not_found(_e: HTTPException) -> tuple[str, int]:
|
|
191
|
+
"""Handle 404 Not Found errors.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
_e: HTTPException object containing error details (unused)
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Tuple of rendered 404 template and HTTP 404 status code
|
|
198
|
+
"""
|
|
85
199
|
return render_template("404.html", title="404"), 404
|
|
86
200
|
|
|
87
201
|
return plugify(app)
|
|
88
202
|
|
|
89
203
|
|
|
90
204
|
def create_app_from_config(config: Config) -> Engine:
|
|
205
|
+
"""Create a fully configured Platzky application from a Config object.
|
|
206
|
+
|
|
207
|
+
Initializes the database, creates the engine, sets up telemetry (if enabled),
|
|
208
|
+
registers blueprints (admin, blog, SEO), and configures minification and CSRF
|
|
209
|
+
protection.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
config: Application configuration object
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Fully configured Engine instance ready to serve requests
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ImportError: If telemetry is enabled but OpenTelemetry packages are not installed
|
|
219
|
+
ValueError: If telemetry configuration is invalid
|
|
220
|
+
"""
|
|
91
221
|
db = get_db(config.db)
|
|
92
222
|
engine = create_engine(config, db)
|
|
93
223
|
|
|
@@ -133,5 +263,21 @@ def create_app_from_config(config: Config) -> Engine:
|
|
|
133
263
|
|
|
134
264
|
|
|
135
265
|
def create_app(config_path: str) -> Engine:
|
|
266
|
+
"""Create a Platzky application from a YAML configuration file.
|
|
267
|
+
|
|
268
|
+
Convenience function that loads configuration from a YAML file and
|
|
269
|
+
creates the application.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
config_path: Path to the YAML configuration file
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Fully configured Engine instance ready to serve requests
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
FileNotFoundError: If the configuration file doesn't exist
|
|
279
|
+
yaml.YAMLError: If the configuration file contains invalid YAML
|
|
280
|
+
ValidationError: If the configuration doesn't match the expected schema
|
|
281
|
+
"""
|
|
136
282
|
config = Config.parse_yaml(config_path)
|
|
137
283
|
return create_app_from_config(config)
|
platzky/plugin/plugin.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import logging
|
|
3
|
+
import os
|
|
4
|
+
import types
|
|
2
5
|
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Generic, Optional, TypeVar
|
|
4
7
|
|
|
5
8
|
from pydantic import BaseModel, ConfigDict
|
|
6
9
|
|
|
@@ -39,17 +42,50 @@ class PluginBase(Generic[T], ABC):
|
|
|
39
42
|
Plugin developers must extend this class to implement their plugins.
|
|
40
43
|
"""
|
|
41
44
|
|
|
45
|
+
@staticmethod
|
|
46
|
+
def get_locale_dir_from_module(plugin_module: types.ModuleType) -> Optional[str]:
|
|
47
|
+
"""Get plugin locale directory from a module.
|
|
48
|
+
|
|
49
|
+
Encapsulates the knowledge of how plugins organize their locale directories.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
plugin_module: The plugin module
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to the locale directory if it exists, None otherwise
|
|
56
|
+
"""
|
|
57
|
+
if not hasattr(plugin_module, "__file__") or plugin_module.__file__ is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
# Use realpath to resolve symlinks and get canonical path
|
|
61
|
+
plugin_dir = os.path.dirname(os.path.realpath(plugin_module.__file__))
|
|
62
|
+
locale_dir = os.path.join(plugin_dir, "locale")
|
|
63
|
+
|
|
64
|
+
return locale_dir if os.path.isdir(locale_dir) else None
|
|
65
|
+
|
|
42
66
|
@classmethod
|
|
43
|
-
def get_config_model(cls) ->
|
|
67
|
+
def get_config_model(cls) -> type[PluginBaseConfig]:
|
|
44
68
|
return PluginBaseConfig
|
|
45
69
|
|
|
46
|
-
def __init__(self, config:
|
|
70
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
47
71
|
try:
|
|
48
72
|
config_class = self.get_config_model()
|
|
49
73
|
self.config = config_class.model_validate(config)
|
|
50
74
|
except Exception as e:
|
|
51
75
|
raise ConfigPluginError(f"Invalid configuration: {e}") from e
|
|
52
76
|
|
|
77
|
+
def get_locale_dir(self) -> Optional[str]:
|
|
78
|
+
"""Get this plugin's locale directory.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Path to the locale directory if it exists, None otherwise
|
|
82
|
+
"""
|
|
83
|
+
module = inspect.getmodule(self.__class__)
|
|
84
|
+
if module is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
return self.get_locale_dir_from_module(module)
|
|
88
|
+
|
|
53
89
|
@abstractmethod
|
|
54
90
|
def process(self, app: PlatzkyEngine) -> PlatzkyEngine:
|
|
55
91
|
"""Process the plugin with the given app.
|
platzky/plugin/plugin_loader.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import importlib.util
|
|
2
2
|
import inspect
|
|
3
3
|
import logging
|
|
4
|
+
import os
|
|
5
|
+
from types import ModuleType
|
|
4
6
|
from typing import Any, Optional, Type
|
|
5
7
|
|
|
6
8
|
import deprecation
|
|
@@ -11,7 +13,7 @@ from platzky.plugin.plugin import PluginBase, PluginError
|
|
|
11
13
|
logger = logging.getLogger(__name__)
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
def find_plugin(plugin_name: str) ->
|
|
16
|
+
def find_plugin(plugin_name: str) -> ModuleType:
|
|
15
17
|
"""Find plugin by name and return it as module.
|
|
16
18
|
|
|
17
19
|
Args:
|
|
@@ -32,7 +34,7 @@ def find_plugin(plugin_name: str) -> Any:
|
|
|
32
34
|
) from e
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
def _is_class_plugin(plugin_module:
|
|
37
|
+
def _is_class_plugin(plugin_module: ModuleType) -> Optional[Type[PluginBase[Any]]]:
|
|
36
38
|
"""Check if the plugin module contains a PluginBase implementation.
|
|
37
39
|
|
|
38
40
|
Args:
|
|
@@ -49,24 +51,114 @@ def _is_class_plugin(plugin_module: Any) -> Optional[Type[PluginBase[Any]]]:
|
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
@deprecation.deprecated(
|
|
52
|
-
deprecated_in="
|
|
53
|
-
removed_in="0.
|
|
54
|
-
current_version=
|
|
55
|
-
details=
|
|
56
|
-
|
|
54
|
+
deprecated_in="1.2.0",
|
|
55
|
+
removed_in="2.0.0",
|
|
56
|
+
current_version="1.2.0",
|
|
57
|
+
details=(
|
|
58
|
+
"Legacy plugin style using the entrypoint process() function is deprecated. "
|
|
59
|
+
"Migrate to PluginBase to support plugin translations and other features. "
|
|
60
|
+
"See: https://platzky.readthedocs.io/en/latest/plugins.html"
|
|
61
|
+
),
|
|
57
62
|
)
|
|
58
|
-
def _process_legacy_plugin(
|
|
59
|
-
|
|
63
|
+
def _process_legacy_plugin(
|
|
64
|
+
plugin_module: ModuleType, app: Engine, plugin_config: dict[str, Any], plugin_name: str
|
|
65
|
+
) -> Engine:
|
|
66
|
+
"""Process a legacy plugin using the entrypoint approach.
|
|
67
|
+
|
|
68
|
+
DEPRECATED: This function will be removed in version 2.0.0.
|
|
69
|
+
Please migrate your plugin to extend PluginBase.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
plugin_module: The plugin module
|
|
73
|
+
app: The Platzky Engine instance
|
|
74
|
+
plugin_config: Plugin configuration dictionary
|
|
75
|
+
plugin_name: Name of the plugin
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Platzky Engine with processed plugin
|
|
79
|
+
"""
|
|
60
80
|
app = plugin_module.process(app, plugin_config)
|
|
61
|
-
logger.
|
|
81
|
+
logger.warning(
|
|
82
|
+
"Plugin '%s' uses deprecated legacy interface. "
|
|
83
|
+
"This will be removed in version 2.0.0. "
|
|
84
|
+
"Migrate to PluginBase: https://platzky.readthedocs.io/",
|
|
85
|
+
plugin_name,
|
|
86
|
+
)
|
|
62
87
|
return app
|
|
63
88
|
|
|
64
89
|
|
|
90
|
+
def _is_safe_locale_dir(locale_dir: str, plugin_instance: PluginBase[Any]) -> bool:
|
|
91
|
+
"""Validate that a locale directory is safe to use.
|
|
92
|
+
|
|
93
|
+
Prevents malicious plugins from exposing arbitrary filesystem paths
|
|
94
|
+
by ensuring the locale directory is within the plugin's module directory.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
locale_dir: Path to the locale directory
|
|
98
|
+
plugin_instance: The plugin instance
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if the locale directory is safe to use, False otherwise
|
|
102
|
+
"""
|
|
103
|
+
if not os.path.isdir(locale_dir):
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
module = inspect.getmodule(plugin_instance.__class__)
|
|
107
|
+
if module is None or not hasattr(module, "__file__") or module.__file__ is None:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
normalized_path = os.path.normpath(locale_dir)
|
|
111
|
+
if ".." in normalized_path.split(os.sep):
|
|
112
|
+
logger.warning("Rejected locale path with .. components: %s", locale_dir)
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# Get canonical paths (resolve symlinks)
|
|
116
|
+
locale_path = os.path.realpath(locale_dir)
|
|
117
|
+
module_path = os.path.realpath(os.path.dirname(module.__file__))
|
|
118
|
+
|
|
119
|
+
if not locale_path.startswith(module_path + os.sep):
|
|
120
|
+
if locale_path != module_path:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _register_plugin_locale(
|
|
127
|
+
app: Engine, plugin_instance: PluginBase[Any], plugin_name: str
|
|
128
|
+
) -> None:
|
|
129
|
+
"""Register plugin's locale directory with Babel if it exists.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
app: The Platzky Engine instance
|
|
133
|
+
plugin_instance: The plugin instance
|
|
134
|
+
plugin_name: Name of the plugin for logging
|
|
135
|
+
"""
|
|
136
|
+
locale_dir = plugin_instance.get_locale_dir()
|
|
137
|
+
if locale_dir is None:
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Validate that the locale directory is safe to use
|
|
141
|
+
if not _is_safe_locale_dir(locale_dir, plugin_instance):
|
|
142
|
+
logger.warning(
|
|
143
|
+
"Skipping locale directory for plugin %s: path validation failed: %s",
|
|
144
|
+
plugin_name,
|
|
145
|
+
locale_dir,
|
|
146
|
+
)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
babel_config = app.extensions.get("babel")
|
|
150
|
+
if babel_config and locale_dir not in babel_config.translation_directories:
|
|
151
|
+
babel_config.translation_directories.append(locale_dir)
|
|
152
|
+
logger.info("Registered locale directory for plugin %s: %s", plugin_name, locale_dir)
|
|
153
|
+
|
|
154
|
+
|
|
65
155
|
def plugify(app: Engine) -> Engine:
|
|
66
156
|
"""Load plugins and run their entrypoints.
|
|
67
157
|
|
|
68
158
|
Supports both class-based plugins (PluginBase) and legacy entrypoint plugins.
|
|
69
159
|
|
|
160
|
+
Legacy plugin support is deprecated and will be removed in version 2.0.0.
|
|
161
|
+
|
|
70
162
|
Args:
|
|
71
163
|
app: Platzky Engine instance
|
|
72
164
|
|
|
@@ -91,8 +183,9 @@ def plugify(app: Engine) -> Engine:
|
|
|
91
183
|
if plugin_class:
|
|
92
184
|
# Handle new class-based plugins
|
|
93
185
|
plugin_instance = plugin_class(plugin_config)
|
|
186
|
+
_register_plugin_locale(app, plugin_instance, plugin_name)
|
|
94
187
|
app = plugin_instance.process(app)
|
|
95
|
-
logger.info(
|
|
188
|
+
logger.info("Processed class-based plugin: %s", plugin_name)
|
|
96
189
|
elif hasattr(plugin_module, "process"):
|
|
97
190
|
# Handle legacy entrypoint plugins with deprecation warning
|
|
98
191
|
app = _process_legacy_plugin(plugin_module, app, plugin_config, plugin_name)
|
|
@@ -102,8 +195,11 @@ def plugify(app: Engine) -> Engine:
|
|
|
102
195
|
f"or provide a process() function"
|
|
103
196
|
)
|
|
104
197
|
|
|
198
|
+
except PluginError:
|
|
199
|
+
# Re-raise PluginError directly to avoid redundant wrapping
|
|
200
|
+
raise
|
|
105
201
|
except Exception as e:
|
|
106
|
-
logger.
|
|
202
|
+
logger.exception("Error processing plugin %s", plugin_name)
|
|
107
203
|
raise PluginError(f"Error processing plugin {plugin_name}: {e}") from e
|
|
108
204
|
|
|
109
205
|
return app
|
platzky/seo/seo.py
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
"""Flask blueprint for SEO functionality including robots.txt and sitemap.xml."""
|
|
2
|
+
|
|
1
3
|
import typing as t
|
|
2
4
|
import urllib.parse
|
|
3
5
|
from os.path import dirname
|
|
4
6
|
|
|
5
|
-
from flask import Blueprint, current_app, make_response, render_template, request
|
|
7
|
+
from flask import Blueprint, Response, current_app, make_response, render_template, request
|
|
8
|
+
|
|
9
|
+
from platzky.db.db import DB
|
|
10
|
+
|
|
6
11
|
|
|
12
|
+
def create_seo_blueprint(
|
|
13
|
+
db: DB, config: dict[str, t.Any], locale_func: t.Callable[[], str]
|
|
14
|
+
) -> Blueprint:
|
|
15
|
+
"""Create SEO blueprint with routes for robots.txt and sitemap.xml.
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
Args:
|
|
18
|
+
db: Database instance for accessing blog content
|
|
19
|
+
config: Configuration dictionary with SEO and blog settings
|
|
20
|
+
locale_func: Function that returns the current locale/language code
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Configured Flask Blueprint for SEO functionality
|
|
24
|
+
"""
|
|
9
25
|
seo = Blueprint(
|
|
10
26
|
"seo",
|
|
11
27
|
__name__,
|
|
@@ -14,30 +30,49 @@ def create_seo_blueprint(db, config: dict[str, t.Any], locale_func: t.Callable[[
|
|
|
14
30
|
)
|
|
15
31
|
|
|
16
32
|
@seo.route("/robots.txt")
|
|
17
|
-
def robots():
|
|
33
|
+
def robots() -> Response:
|
|
34
|
+
"""Generate robots.txt file for search engine crawlers.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Text response containing robots.txt directives
|
|
38
|
+
"""
|
|
18
39
|
robots_response = render_template("robots.txt", domain=request.host, mimetype="text/plain")
|
|
19
40
|
response = make_response(robots_response)
|
|
20
41
|
response.headers["Content-Type"] = "text/plain"
|
|
21
42
|
return response
|
|
22
43
|
|
|
23
|
-
def get_blog_entries(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
def get_blog_entries(
|
|
45
|
+
host_base: str, lang: str, db: DB, blog_prefix: str
|
|
46
|
+
) -> list[dict[str, str]]:
|
|
47
|
+
"""Generate sitemap entries for all blog posts.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
host_base: Base URL of the website (e.g., 'https://example.com')
|
|
51
|
+
lang: Language code for posts to include
|
|
52
|
+
db: Database instance for accessing blog posts
|
|
53
|
+
blog_prefix: URL prefix for blog routes
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of dictionaries with sitemap URL entries (loc, lastmod)
|
|
57
|
+
"""
|
|
58
|
+
dynamic_urls = []
|
|
59
|
+
# TODO: Add get_list_of_posts for faster getting just list of it
|
|
60
|
+
for post in db.get_all_posts(lang):
|
|
29
61
|
slug = post.slug
|
|
30
|
-
datet = post.date.
|
|
62
|
+
datet = post.date.date().isoformat()
|
|
31
63
|
url = {"loc": f"{host_base}{blog_prefix}/{slug}", "lastmod": datet}
|
|
32
64
|
dynamic_urls.append(url)
|
|
33
65
|
return dynamic_urls
|
|
34
66
|
|
|
35
|
-
@seo.route("/sitemap.xml") # TODO
|
|
36
|
-
def sitemap():
|
|
37
|
-
"""
|
|
38
|
-
|
|
67
|
+
@seo.route("/sitemap.xml") # TODO: Try to replace sitemap logic with flask-sitemap module
|
|
68
|
+
def sitemap() -> Response:
|
|
69
|
+
"""Route to dynamically generate a sitemap of your website/application.
|
|
70
|
+
|
|
39
71
|
lastmod and priority tags omitted on static pages.
|
|
40
|
-
lastmod included on dynamic content such as
|
|
72
|
+
lastmod included on dynamic content such as blog posts.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
XML response containing the sitemap
|
|
41
76
|
"""
|
|
42
77
|
lang = locale_func()
|
|
43
78
|
|
|
@@ -46,7 +81,7 @@ def create_seo_blueprint(db, config: dict[str, t.Any], locale_func: t.Callable[[
|
|
|
46
81
|
host_base = host_components.scheme + "://" + host_components.netloc
|
|
47
82
|
|
|
48
83
|
# Static routes with static content
|
|
49
|
-
static_urls =
|
|
84
|
+
static_urls = []
|
|
50
85
|
for rule in current_app.url_map.iter_rules():
|
|
51
86
|
if rule.methods is not None and "GET" in rule.methods and len(rule.arguments) == 0:
|
|
52
87
|
url = {"loc": f"{host_base}{rule!s}"}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
platzky/__init__.py,sha256=IhL91rSWxIIJQNfVsqJ1d4yY5D2WyWcefo4Xv2aX_lo,180
|
|
2
|
-
platzky/admin/admin.py,sha256=
|
|
3
|
-
platzky/admin/fake_login.py,sha256=
|
|
2
|
+
platzky/admin/admin.py,sha256=QhuxGtUjfX-xeDd_xmSChoeD5Z1UMu1jTGtUck-9jJU,1699
|
|
3
|
+
platzky/admin/fake_login.py,sha256=Z_4M4PLQ73qL-sKh05CmDx_nFy8S30PdsNfPPDeFSmE,3528
|
|
4
4
|
platzky/admin/templates/admin.html,sha256=zgjROhSezayZqnNFezvVa0MEfgmXLvOM8HRRaZemkQw,688
|
|
5
5
|
platzky/admin/templates/login.html,sha256=oBNuv130iMTwXrtRnDUDcGIGvu0O2VsIbjQxw-Tjd7Y,380
|
|
6
6
|
platzky/admin/templates/module.html,sha256=WuQZxKQDD4INl-QF2uiKHf9Fmf2h7cEW9RLe1nWKC8k,175
|
|
7
7
|
platzky/blog/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
platzky/blog/blog.py,sha256=
|
|
9
|
-
platzky/blog/comment_form.py,sha256=
|
|
10
|
-
platzky/config.py,sha256=
|
|
8
|
+
platzky/blog/blog.py,sha256=n3bsZ1GpVCmvxCFMiF7QUDb_PHbmBiTu0GDu3r_Su24,5490
|
|
9
|
+
platzky/blog/comment_form.py,sha256=yOuXvX9PZLc6qQLIWZWLFcbwFQD4a849X82PlXKUzdk,805
|
|
10
|
+
platzky/config.py,sha256=_TQNZ8w8-xQImtm6Gw2SawBqf-UFxF9okIlZi_DGrGA,7540
|
|
11
11
|
platzky/db/README.md,sha256=IO-LoDsd4dLBZenaz423EZjvEOQu_8m2OC0G7du170w,1753
|
|
12
12
|
platzky/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
platzky/db/db.py,sha256=
|
|
14
|
-
platzky/db/db_loader.py,sha256=
|
|
15
|
-
platzky/db/github_json_db.py,sha256=
|
|
16
|
-
platzky/db/google_json_db.py,sha256=
|
|
17
|
-
platzky/db/graph_ql_db.py,sha256=
|
|
18
|
-
platzky/db/json_db.py,sha256=
|
|
19
|
-
platzky/db/json_file_db.py,sha256=
|
|
20
|
-
platzky/db/mongodb_db.py,sha256=
|
|
21
|
-
platzky/engine.py,sha256=
|
|
13
|
+
platzky/db/db.py,sha256=gi5uxvY8Ww8O4y2rxaH1Zj_12Yno8SbILvIaWnQPbYQ,4778
|
|
14
|
+
platzky/db/db_loader.py,sha256=YgR16K5Mj5pN0vWYVQxTD4Z6ihG5fZyjbUUCS8LNqJs,999
|
|
15
|
+
platzky/db/github_json_db.py,sha256=0z-aCz7Pm6Al--SbIHx4T_FyzwfwQcZDqBduTCOE5_A,3314
|
|
16
|
+
platzky/db/google_json_db.py,sha256=RnvirGFo5a41vkd1iD-M4-HwHlWDZ19EqpF0vIer_Xo,3049
|
|
17
|
+
platzky/db/graph_ql_db.py,sha256=a8LGPJKoNpmTkJ6Bb89eg6m6Q9GFetp5z8A0xuemuSk,14504
|
|
18
|
+
platzky/db/json_db.py,sha256=pANXJZzVPAO890TRy3IvzHjpCaeDgNNgNOR5Uhkk2h4,8078
|
|
19
|
+
platzky/db/json_file_db.py,sha256=Tl6b67p4hNViXSAjujXZ9vtVHN61QhrzJUgOuvngyMI,2232
|
|
20
|
+
platzky/db/mongodb_db.py,sha256=nq07j0NldK014qRL1mF-cvBXQF1LKKTTeWxaZdyzjAs,8595
|
|
21
|
+
platzky/engine.py,sha256=9Y74nrO4gwr9_CqRTzPs9LtcY2t0fs0XtPyjPiqRlVQ,5573
|
|
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
|
-
platzky/models.py,sha256=
|
|
25
|
-
platzky/platzky.py,sha256=
|
|
26
|
-
platzky/plugin/plugin.py,sha256=
|
|
27
|
-
platzky/plugin/plugin_loader.py,sha256=
|
|
28
|
-
platzky/seo/seo.py,sha256=
|
|
24
|
+
platzky/models.py,sha256=Z372NhIhZcJ92DLPlOq44gTu8XqVcw05SOeJ1BaU7zE,6767
|
|
25
|
+
platzky/platzky.py,sha256=1LKYq8pLm1QBlOcEPhugxWi8W0vuWqjjINIFK8b2Kow,9319
|
|
26
|
+
platzky/plugin/plugin.py,sha256=KZb6VEph__lx9xrv5Ay4h4XkFFYbodV5OimaG6B9IDc,2812
|
|
27
|
+
platzky/plugin/plugin_loader.py,sha256=eKG6zodUCkiRLxJ2ZX9zdN4-ZrZ9EwssoY1SDtThaFo,6707
|
|
28
|
+
platzky/seo/seo.py,sha256=yEyoRzXNXV9lyqnHSGW8mewC3_vYzyQFHI3EuRrd8ao,3805
|
|
29
29
|
platzky/static/blog.css,sha256=TrppzgQbj4UtuTufDCdblyNTVAqgIbhD66Cziyv_xnY,7893
|
|
30
30
|
platzky/static/styles.css,sha256=U5ddGIK-VcGRJZ3BdOpMp0pR__k6rNEMsuQXkP4tFQ0,686
|
|
31
31
|
platzky/telemetry.py,sha256=iXYvEt0Uw5Hx8lAxyr45dpQ_SiE2NxmJkoSx-JSRJyM,5011
|
|
@@ -41,7 +41,7 @@ platzky/templates/post.html,sha256=GSgjIZsOQKtNx3cEbquSjZ5L4whPnG6MzRyoq9k4B8Q,1
|
|
|
41
41
|
platzky/templates/robots.txt,sha256=2_j2tiYtYJnzZUrANiX9pvBxyw5Dp27fR_co18BPEJ0,116
|
|
42
42
|
platzky/templates/sitemap.xml,sha256=iIJZ91_B5ZuNLCHsRtsGKZlBAXojOTP8kffqKLacgvs,578
|
|
43
43
|
platzky/www_handler.py,sha256=pF6Rmvem1sdVqHD7z3RLrDuG-CwAqfGCti50_NPsB2w,725
|
|
44
|
-
platzky-1.0.
|
|
45
|
-
platzky-1.0.
|
|
46
|
-
platzky-1.0.
|
|
47
|
-
platzky-1.0.
|
|
44
|
+
platzky-1.2.0.dist-info/METADATA,sha256=N32Pfi0ph7na4fWV4U-mM9B89BwCTtMCErfDpFJDU1I,2556
|
|
45
|
+
platzky-1.2.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
46
|
+
platzky-1.2.0.dist-info/licenses/LICENSE,sha256=wCdfk-qEosi6BDwiBulMfKMi0hxp1UXV0DdjLrRm788,1077
|
|
47
|
+
platzky-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|