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/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 create_engine(config: Config, db) -> Engine:
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
- else:
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
- if new_domain := get_langs_domain(lang):
46
- return redirect("http://" + new_domain, code=301)
47
- else:
48
- session["language"] = lang
49
- return redirect(request.referrer)
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
- return urllib.parse.quote(x, safe="")
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
- flag = lang.flag if (lang := config.languages.get(locale)) is not None else ""
58
- country = lang.country if (lang := config.languages.get(locale)) is not None else ""
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(e):
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, Dict, Generic, Type, TypeVar
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) -> Type[PluginBaseConfig]:
67
+ def get_config_model(cls) -> type[PluginBaseConfig]:
44
68
  return PluginBaseConfig
45
69
 
46
- def __init__(self, config: Dict[str, Any]):
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.
@@ -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) -> Any:
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: Any) -> Optional[Type[PluginBase[Any]]]:
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="0.3.1",
53
- removed_in="0.4.0",
54
- current_version=None, # You should replace this with the current version
55
- details="Legacy plugin style using the entrypoint process() function is deprecated. "
56
- "Please migrate to the PluginBase interface.",
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(plugin_module, app, plugin_config, plugin_name):
59
- """Process a legacy plugin using the entrypoint approach."""
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.info(f"Processed legacy plugin: {plugin_name}")
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(f"Processed class-based plugin: {plugin_name}")
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.error(f"Error processing plugin {plugin_name}: {e}")
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
- def create_seo_blueprint(db, config: dict[str, t.Any], locale_func: t.Callable[[], str]):
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(host_base, lang, db, blog_prefix):
24
- dynamic_urls = list()
25
- print(blog_prefix)
26
- for post in db.get_all_posts(
27
- lang
28
- ): # TODO add get_list_of_posts for faster getting just list of it
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.split("T")[0]
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 try to replace sitemap logic with flask-sitemap module
36
- def sitemap():
37
- """
38
- Route to dynamically generate a sitemap of your website/application.
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 seo posts.
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 = list()
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: platzky
3
- Version: 1.0.1
3
+ Version: 1.2.0
4
4
  Summary: Not only blog engine
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,31 +1,31 @@
1
1
  platzky/__init__.py,sha256=IhL91rSWxIIJQNfVsqJ1d4yY5D2WyWcefo4Xv2aX_lo,180
2
- platzky/admin/admin.py,sha256=PlwAXaR_YaM07knB88D33vNe6vCpJXOfuxpCaGnHQlY,1028
3
- platzky/admin/fake_login.py,sha256=tGsdofPyI3P65pol_3Nbj5075xjA_CU090brnBVNjxg,3480
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=L9IYWxnLo1v1h_wLF-0HyG1Y4RSGg7maEMnYxhTgG5Y,2971
9
- platzky/blog/comment_form.py,sha256=4lkNJ_S_2DZmJBbz-NPDqahvy2Zz5AGNH2spFeGIop4,513
10
- platzky/config.py,sha256=NFnW-cjieWgER-uRko1Hbuh6cyGKCFQcq6ihDNSyjQk,7529
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=0h5rGCBO_N1wBqJRl5EoiW_bFDpNIvmNwuA0hJi89jw,3060
14
- platzky/db/db_loader.py,sha256=CuEiXxhIa4bFMm0vi7ugzm7j3WycilGRKCU6smgIImE,905
15
- platzky/db/github_json_db.py,sha256=G1GBIomeKOCeG05pA4qccaFntiGzkgyEMQJz_FQlvNY,2185
16
- platzky/db/google_json_db.py,sha256=rS__UEK7ed71htTg066_vzpg0etTlpke6YkcrAQ3Fgk,1325
17
- platzky/db/graph_ql_db.py,sha256=af6yy1R27YO8N9zJWU7VgU7optRgpdk_1ZUtab_1eT4,8967
18
- platzky/db/json_db.py,sha256=NUBPy4jt-y37TYq4SCGaSgief3MbBWL_Efw8Bxp8Jo0,4046
19
- platzky/db/json_file_db.py,sha256=tPo92n5zG7vGpunn5vl66zISHBziQdxBttitvc5hPug,1030
20
- platzky/db/mongodb_db.py,sha256=28KO8XmTEiqE7FcNBzw_pfxOy6Vo-T7qsHdUlh59QX0,5174
21
- platzky/engine.py,sha256=0bxKzfK83ic-VNlKcDut_84_u5EmY2baU5JcJsleUoM,5461
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=DZZgKW2Q3fY2GMdikFUmAgpsRqT5VKAOwP6RmEsmO2M,1871
25
- platzky/platzky.py,sha256=8mTqdYqRKONv2oGvQF5Y0fuOomH7ZFYvXtxc7ZohBLE,4585
26
- platzky/plugin/plugin.py,sha256=tV8aobIzMDJe1frKUAi4kLbrTAIS0FWE3oYpktSo6Ug,1633
27
- platzky/plugin/plugin_loader.py,sha256=MeQ8LNbrOomwXgc1ISHuyhjZd2mzYKen70eDShWs-Co,3497
28
- platzky/seo/seo.py,sha256=N_MmAA4KJZmmrDUh0hYNtD8ycOwpNKow4gVSAv8V3N4,2631
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.1.dist-info/METADATA,sha256=lkAvxX2DdR4HSpfYr7A5TXnStXzv0O4ZqFqhDIwrD_c,2556
45
- platzky-1.0.1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
46
- platzky-1.0.1.dist-info/licenses/LICENSE,sha256=wCdfk-qEosi6BDwiBulMfKMi0hxp1UXV0DdjLrRm788,1077
47
- platzky-1.0.1.dist-info/RECORD,,
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,,