platzky 0.3.0__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {platzky-0.3.0 → platzky-0.3.3}/PKG-INFO +3 -1
  2. platzky-0.3.3/platzky/__init__.py +3 -0
  3. platzky-0.3.3/platzky/admin/admin.py +33 -0
  4. platzky-0.3.3/platzky/admin/fake_login.py +82 -0
  5. platzky-0.3.3/platzky/admin/templates/admin.html +30 -0
  6. platzky-0.3.3/platzky/admin/templates/login.html +23 -0
  7. platzky-0.3.3/platzky/admin/templates/module.html +9 -0
  8. {platzky-0.3.0 → platzky-0.3.3}/platzky/blog/blog.py +0 -1
  9. platzky-0.3.3/platzky/db/README.md +70 -0
  10. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/db.py +1 -1
  11. platzky-0.3.3/platzky/db/github_json_db.py +69 -0
  12. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/google_json_db.py +2 -7
  13. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/graph_ql_db.py +4 -4
  14. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/json_db.py +7 -3
  15. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/json_file_db.py +2 -2
  16. platzky-0.3.3/platzky/engine.py +61 -0
  17. {platzky-0.3.0 → platzky-0.3.3}/platzky/platzky.py +23 -67
  18. platzky-0.3.3/platzky/plugin/plugin.py +66 -0
  19. platzky-0.3.3/platzky/plugin/plugin_loader.py +109 -0
  20. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/base.html +11 -0
  21. {platzky-0.3.0 → platzky-0.3.3}/pyproject.toml +3 -1
  22. platzky-0.3.0/platzky/__init__.py +0 -3
  23. platzky-0.3.0/platzky/plugin_loader.py +0 -43
  24. {platzky-0.3.0 → platzky-0.3.3}/README.md +0 -0
  25. {platzky-0.3.0 → platzky-0.3.3}/platzky/blog/__init__.py +0 -0
  26. {platzky-0.3.0 → platzky-0.3.3}/platzky/blog/comment_form.py +0 -0
  27. {platzky-0.3.0 → platzky-0.3.3}/platzky/config.py +0 -0
  28. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/__init__.py +0 -0
  29. {platzky-0.3.0 → platzky-0.3.3}/platzky/db/db_loader.py +0 -0
  30. {platzky-0.3.0 → platzky-0.3.3}/platzky/locale/en/LC_MESSAGES/messages.po +0 -0
  31. {platzky-0.3.0 → platzky-0.3.3}/platzky/locale/pl/LC_MESSAGES/messages.po +0 -0
  32. {platzky-0.3.0 → platzky-0.3.3}/platzky/models.py +0 -0
  33. {platzky-0.3.0 → platzky-0.3.3}/platzky/seo/seo.py +0 -0
  34. {platzky-0.3.0 → platzky-0.3.3}/platzky/static/blog.css +0 -0
  35. {platzky-0.3.0 → platzky-0.3.3}/platzky/static/styles.css +0 -0
  36. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/404.html +0 -0
  37. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/blog.html +0 -0
  38. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/body_meta.html +0 -0
  39. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/dynamic_css.html +0 -0
  40. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/feed.xml +0 -0
  41. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/head_meta.html +0 -0
  42. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/page.html +0 -0
  43. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/post.html +0 -0
  44. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/robots.txt +0 -0
  45. {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/sitemap.xml +0 -0
  46. {platzky-0.3.0 → platzky-0.3.3}/platzky/www_handler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: platzky
3
- Version: 0.3.0
3
+ Version: 0.3.3
4
4
  Summary: Not only blog engine
5
5
  License: MIT
6
6
  Requires-Python: >=3.10,<4.0
@@ -16,10 +16,12 @@ Requires-Dist: Flask-Minify (>=0.42,<0.43)
16
16
  Requires-Dist: Flask-WTF (>=1.2.1,<2.0.0)
17
17
  Requires-Dist: PyYAML (>=6.0,<7.0)
18
18
  Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
19
+ Requires-Dist: deprecation (>=2.1.0,<3.0.0)
19
20
  Requires-Dist: google-cloud-storage (>=2.5.0,<3.0.0)
20
21
  Requires-Dist: gql (>=3.4.0,<4.0.0)
21
22
  Requires-Dist: humanize (>=4.9.0,<5.0.0)
22
23
  Requires-Dist: pydantic (>=2.7.1,<3.0.0)
24
+ Requires-Dist: pygithub (>=2.6.1,<3.0.0)
23
25
  Description-Content-Type: text/markdown
24
26
 
25
27
  ![Github Actions](https://github.com/platzky/platzky/actions/workflows/tests.yml/badge.svg?event=push&branch=main)
@@ -0,0 +1,3 @@
1
+ from platzky.engine import Engine as Engine
2
+ from platzky.platzky import create_app_from_config as create_app_from_config
3
+ from platzky.platzky import create_engine as create_engine
@@ -0,0 +1,33 @@
1
+ from os.path import dirname
2
+
3
+ from flask import Blueprint, render_template, session
4
+
5
+
6
+ def create_admin_blueprint(login_methods, db, locale_func):
7
+ admin = Blueprint(
8
+ "admin",
9
+ __name__,
10
+ url_prefix="/admin",
11
+ template_folder=f"{dirname(__file__)}/templates",
12
+ )
13
+
14
+ @admin.route("/", methods=["GET"])
15
+ def admin_panel_home():
16
+ user = session.get("user", None)
17
+
18
+ if not user:
19
+ return render_template("login.html", login_methods=login_methods)
20
+
21
+ cms_modules = {"plugins": [plugin.get("name") for plugin in db.get_plugins_data()]}
22
+ return render_template("admin.html", user=user, cms_modules=cms_modules)
23
+
24
+ @admin.route("/module/<module_name>", methods=["GET"])
25
+ def module_settings(module_name):
26
+ user = session.get("user", None)
27
+
28
+ if not user:
29
+ return render_template("login.html", login_methods=login_methods)
30
+
31
+ return render_template("module.html", user=user, module_name=module_name)
32
+
33
+ return admin
@@ -0,0 +1,82 @@
1
+ """
2
+ Fake login functionality for development environments only.
3
+
4
+ WARNING: This module provides fake login functionality and should NEVER be used in production
5
+ environments as it bypasses proper authentication and authorization controls.
6
+ """
7
+
8
+ import os
9
+ from typing import Any, Callable
10
+
11
+ from flask import Blueprint, flash, redirect, session, url_for
12
+ from markupsafe import Markup
13
+
14
+ ROLE_ADMIN = "admin"
15
+ ROLE_NONADMIN = "nonadmin"
16
+ VALID_ROLES = [ROLE_ADMIN, ROLE_NONADMIN]
17
+
18
+
19
+ def get_fake_login_html() -> Callable[[], str]:
20
+ """Return a callable that generates HTML for fake login buttons."""
21
+
22
+ def generate_html() -> str:
23
+ admin_url = url_for("admin.handle_fake_login", role="admin")
24
+ nonadmin_url = url_for("admin.handle_fake_login", role="nonadmin")
25
+
26
+ # Rest of the code remains the same
27
+ html = f"""
28
+ <div class="col-md-6 mb-4">
29
+ <div class="card">
30
+ <div class="card-header">
31
+ Development Login
32
+ </div>
33
+ <div class="card-body">
34
+ <p class="text-danger"><strong>Warning:</strong> For development only</p>
35
+ <div class="d-flex justify-content-around">
36
+ <form method="post" action="{admin_url}" style="display: inline;">
37
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
38
+ <button type="submit" class="btn btn-primary">Login as Admin</button>
39
+ </form>
40
+ <form method="post" action="{nonadmin_url}" style="display: inline;">
41
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
42
+ <button type="submit" class="btn btn-secondary">Login as Non-Admin</button>
43
+ </form>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ """
49
+ return Markup(html)
50
+
51
+ return generate_html
52
+
53
+
54
+ def setup_fake_login_routes(admin_blueprint: Blueprint) -> Blueprint:
55
+ """Add fake login routes to the provided admin_blueprint."""
56
+
57
+ env = os.environ
58
+ is_testing = "PYTEST_CURRENT_TEST" in env.keys() or env.get("FLASK_DEBUG") in (
59
+ "1",
60
+ "true",
61
+ "True",
62
+ True,
63
+ )
64
+
65
+ if not is_testing:
66
+ raise RuntimeError(
67
+ "SECURITY ERROR: Fake login routes are enabled outside of a testing environment! "
68
+ "This functionality must only be used during development or testing."
69
+ )
70
+
71
+ @admin_blueprint.route("/fake-login/<role>", methods=["POST"])
72
+ def handle_fake_login(role: str) -> Any:
73
+ if role not in VALID_ROLES:
74
+ flash(f"Invalid role: {role}. Must be one of: {', '.join(VALID_ROLES)}", "error")
75
+ return redirect(url_for("admin.admin_panel_home"))
76
+ if role == ROLE_ADMIN:
77
+ session["user"] = {"username": ROLE_ADMIN, "role": ROLE_ADMIN}
78
+ else:
79
+ session["user"] = {"username": "user", "role": ROLE_NONADMIN}
80
+ return redirect(url_for("admin.admin_panel_home"))
81
+
82
+ return admin_blueprint
@@ -0,0 +1,30 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block description %}
4
+ {{ _("This is the admin page. You can manage your posts here.") }}
5
+ {% endblock %}
6
+
7
+ {% block content %}
8
+
9
+ <div class="admin-contents mx-auto w-75">
10
+ <h1>{% block title %}Admin{% endblock %}</h1>
11
+ {% if user %}
12
+ <div class="alert alert-success" role="alert">You're logged in</div>
13
+ {% endif %}
14
+ </div>
15
+
16
+ {% endblock %}
17
+
18
+ {% block left_panel %}
19
+ <div id="admin-panel">
20
+ <p>User: {{ user.username }}</p>
21
+ {% for cms_module_name, cms_entries in cms_modules.items() %}
22
+ <div class="cms-module mb-2">
23
+ <p>{{ cms_module_name }}</p>
24
+ {% for cms_entry in cms_entries %}
25
+ <a href="{{ url_for('admin.module', name=cms_entry) }}" class="cms-entry btn btn-primary btn-block mb-2">{{ cms_entry }}</a>
26
+ {% endfor %}
27
+ </div>
28
+ {% endfor %}
29
+ </div>
30
+ {% endblock %}
@@ -0,0 +1,23 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block description %}
4
+ {{ _("This is the login page. You can log in here.") }}
5
+
6
+ {% endblock %}
7
+
8
+ {% block content %}
9
+
10
+ <div class="login-contents mx-auto w-75">
11
+ <h1>{% block title %}Login{% endblock %}</h1>
12
+
13
+ <div class="row align-items-center">
14
+
15
+ {% for method in login_methods %}
16
+ {{ method() }}
17
+
18
+ {% endfor %}
19
+
20
+ </div>
21
+ </div>
22
+
23
+ {% endblock %}
@@ -0,0 +1,9 @@
1
+ {% extends "admin.html" %}
2
+
3
+ {% block content %}
4
+
5
+ <div class="module-contents mx-auto w-75">
6
+ <h1>{% block title %}{% module_name %}{% endblock %}</h1>
7
+ </div>
8
+
9
+ {% endblock %}
@@ -51,7 +51,6 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
51
51
 
52
52
  @blog.route("/<post_slug>", methods=["GET"])
53
53
  def get_post(post_slug):
54
- post = db.get_post(post_slug)
55
54
  try:
56
55
  post = db.get_post(post_slug)
57
56
  return render_template(
@@ -0,0 +1,70 @@
1
+ # Platzky Database Modules
2
+
3
+ This directory contains the database abstraction layer for the Platzky application. The database modules provide a consistent interface for accessing content regardless of where it's stored.
4
+
5
+ ## Architecture
6
+
7
+ The database layer is built on an abstract base class (DB) that defines a common interface. Multiple implementations are provided for different storage backends:
8
+
9
+ - **Json**: Base implementation for JSON data sources
10
+ - **JsonFile**: Local JSON file storage
11
+ - **GithubJsonDb**: JSON files stored in GitHub repository
12
+ - **GoogleJsonDb**: JSON files stored in Google Cloud Storage
13
+ - **GraphQL**: Content stored in a GraphQL API
14
+
15
+
16
+ ## Configuration
17
+
18
+ Database configuration is specified in your application config file. Each database type has its own configuration schema.
19
+
20
+ ### JSON File Database
21
+
22
+ ```yaml
23
+ DB:
24
+ TYPE: json_file
25
+ PATH: "/path/to/data.json"
26
+
27
+ ```
28
+ ### GitHub JSON Database
29
+
30
+ ```yaml
31
+ DB:
32
+ TYPE: github_json
33
+ REPO_NAME: "username/repository"
34
+ GITHUB_TOKEN: "your_github_token"
35
+ BRANCH_NAME: "main"
36
+ PATH_TO_FILE: "data.json"
37
+ ```
38
+
39
+ ### Google JSON Database
40
+
41
+ ```yaml
42
+ DB:
43
+ TYPE: google_json
44
+ BUCKET_NAME: "your-bucket-name"
45
+ SOURCE_BLOB_NAME: "data.json"
46
+ ```
47
+
48
+ ### GraphQL Database
49
+
50
+ ```yaml
51
+ DB:
52
+ TYPE: graph_ql
53
+ CMS_ENDPOINT: "https://your-graphql-endpoint.com/api"
54
+ CMS_TOKEN: "your_graphql_token"
55
+ ```
56
+
57
+ ## Usage
58
+
59
+ The database is automatically initialized based on your configuration. The application will use the appropriate database implementation
60
+
61
+ ```python
62
+ from platzky.db.db_loader import get_db
63
+
64
+ # db_config is loaded from your application config
65
+ db = get_db(db_config)
66
+
67
+ # Now you can use any of the standard DB methods
68
+ posts = db.get_all_posts("en")
69
+ menu_items = db.get_menu_items_in_lang("en")
70
+ ```
@@ -4,7 +4,7 @@ from typing import Any, Callable
4
4
 
5
5
  from pydantic import BaseModel, Field
6
6
 
7
- from ..models import Color, MenuItem, Page, Post
7
+ from platzky.models import Color, MenuItem, Page, Post
8
8
 
9
9
 
10
10
  class DB(ABC):
@@ -0,0 +1,69 @@
1
+ import json
2
+
3
+ import requests
4
+ from github import Github
5
+ from pydantic import Field
6
+
7
+ from platzky.db.db import DBConfig
8
+ from platzky.db.json_db import Json as JsonDB
9
+
10
+
11
+ def db_config_type():
12
+ return GithubJsonDbConfig
13
+
14
+
15
+ class GithubJsonDbConfig(DBConfig):
16
+ github_token: str = Field(alias="GITHUB_TOKEN")
17
+ repo_name: str = Field(alias="REPO_NAME")
18
+ path_to_file: str = Field(alias="PATH_TO_FILE")
19
+ branch_name: str = Field(alias="BRANCH_NAME", default="main")
20
+
21
+
22
+ def db_from_config(config: GithubJsonDbConfig):
23
+ return GithubJsonDb(
24
+ config.github_token, config.repo_name, config.branch_name, config.path_to_file
25
+ )
26
+
27
+
28
+ def get_db(config):
29
+ github_json_db_config = GithubJsonDbConfig.model_validate(config)
30
+ return GithubJsonDb(
31
+ github_json_db_config.github_token,
32
+ github_json_db_config.repo_name,
33
+ github_json_db_config.branch_name,
34
+ github_json_db_config.path_to_file,
35
+ )
36
+
37
+
38
+ class GithubJsonDb(JsonDB):
39
+ def __init__(self, github_token: str, repo_name: str, branch_name: str, path_to_file: str):
40
+ self.branch_name = branch_name
41
+ self.repo = Github(github_token).get_repo(repo_name)
42
+ self.file_path = path_to_file
43
+
44
+ try:
45
+ file_content = self.repo.get_contents(self.file_path, ref=self.branch_name)
46
+
47
+ if isinstance(file_content, list):
48
+ raise ValueError(f"Path '{self.file_path}' points to a directory, not a file")
49
+
50
+ if file_content.content:
51
+ raw_data = file_content.decoded_content.decode("utf-8")
52
+ else:
53
+
54
+ download_url = file_content.download_url
55
+ response = requests.get(download_url, timeout=40)
56
+ response.raise_for_status()
57
+ raw_data = response.text
58
+
59
+ self.data = json.loads(raw_data)
60
+
61
+ except (json.JSONDecodeError, requests.RequestException) as e:
62
+ raise ValueError(f"Error parsing JSON content: {e}")
63
+ except Exception as e:
64
+ raise ValueError(f"Error retrieving GitHub content: {e}")
65
+
66
+ super().__init__(self.data)
67
+
68
+ self.module_name = "github_json_db"
69
+ self.db_name = "GithubJsonDb"
@@ -3,8 +3,8 @@ import json
3
3
  from google.cloud.storage import Client
4
4
  from pydantic import Field
5
5
 
6
- from .db import DBConfig
7
- from .json_db import Json
6
+ from platzky.db.db import DBConfig
7
+ from platzky.db.json_db import Json
8
8
 
9
9
 
10
10
  def db_config_type():
@@ -47,8 +47,3 @@ class GoogleJsonDb(Json):
47
47
 
48
48
  self.module_name = "google_json_db"
49
49
  self.db_name = "GoogleJsonDb"
50
-
51
- def __save_entry(self, entry):
52
- data = get_data(self.blob)
53
- data["data"].append(entry)
54
- self.blob.upload_from_string(json.dumps(data), content_type="application/json")
@@ -6,8 +6,8 @@ from gql.transport.aiohttp import AIOHTTPTransport
6
6
  from gql.transport.exceptions import TransportQueryError
7
7
  from pydantic import Field
8
8
 
9
- from ..models import Color, Post
10
- from .db import DB, DBConfig
9
+ from platzky.db.db import DB, DBConfig
10
+ from platzky.models import Color, Post
11
11
 
12
12
 
13
13
  def db_config_type():
@@ -29,7 +29,7 @@ def db_from_config(config: GraphQlDbConfig):
29
29
 
30
30
  def _standarize_comment(
31
31
  comment,
32
- ): # TODO add tests for checking stadarization of comments
32
+ ):
33
33
  return {
34
34
  "author": comment["author"],
35
35
  "comment": comment["comment"],
@@ -37,7 +37,7 @@ def _standarize_comment(
37
37
  }
38
38
 
39
39
 
40
- def _standarize_post(post): # TODO add tests for checking stadarization of posts
40
+ def _standarize_post(post):
41
41
  return {
42
42
  "author": post["author"]["name"],
43
43
  "slug": post["slug"],
@@ -3,8 +3,8 @@ from typing import Any, Dict
3
3
 
4
4
  from pydantic import Field
5
5
 
6
- from ..models import MenuItem, Post
7
- from .db import DB, DBConfig
6
+ from platzky.db.db import DB, DBConfig
7
+ from platzky.models import MenuItem, Post
8
8
 
9
9
 
10
10
  def db_config_type():
@@ -73,7 +73,11 @@ class Json(DB):
73
73
  return menu_items_list
74
74
 
75
75
  def get_posts_by_tag(self, tag, lang):
76
- return (post for post in self._get_site_content()["posts"] if tag in post["tags"])
76
+ return (
77
+ post
78
+ for post in self._get_site_content()["posts"]
79
+ if tag in post["tags"] and post["language"] == lang
80
+ )
77
81
 
78
82
  def _get_site_content(self):
79
83
  content = self.data.get("site_content")
@@ -2,8 +2,8 @@ import json
2
2
 
3
3
  from pydantic import Field
4
4
 
5
- from .db import DBConfig
6
- from .json_db import Json
5
+ from platzky.db.db import DBConfig
6
+ from platzky.db.json_db import Json
7
7
 
8
8
 
9
9
  def db_config_type():
@@ -0,0 +1,61 @@
1
+ import os
2
+
3
+ from flask import Flask, request, session
4
+ from flask_babel import Babel
5
+
6
+ from platzky.config import Config
7
+
8
+
9
+ class Engine(Flask):
10
+ def __init__(self, config: Config, db, import_name):
11
+ super().__init__(import_name)
12
+ self.config.from_mapping(config.model_dump(by_alias=True))
13
+ self.db = db
14
+ self.notifiers = []
15
+ self.login_methods = []
16
+ self.dynamic_body = ""
17
+ self.dynamic_head = ""
18
+ directory = os.path.dirname(os.path.realpath(__file__))
19
+ locale_dir = os.path.join(directory, "locale")
20
+ config.translation_directories.append(locale_dir)
21
+ babel_translation_directories = ";".join(config.translation_directories)
22
+ self.babel = Babel(
23
+ self,
24
+ locale_selector=self.get_locale,
25
+ default_translation_directories=babel_translation_directories,
26
+ )
27
+
28
+ def notify(self, message: str):
29
+ for notifier in self.notifiers:
30
+ notifier(message)
31
+
32
+ def add_notifier(self, notifier):
33
+ self.notifiers.append(notifier)
34
+
35
+ # TODO login_method should be interface
36
+ def add_login_method(self, login_method):
37
+ self.login_methods.append(login_method)
38
+
39
+ def add_dynamic_body(self, body: str):
40
+ self.dynamic_body += body
41
+
42
+ def add_dynamic_head(self, body: str):
43
+ self.dynamic_head += body
44
+
45
+ def get_locale(self) -> str:
46
+ domain = request.headers.get("Host", "localhost")
47
+ domain_to_lang = self.config.get("DOMAIN_TO_LANG")
48
+
49
+ languages = self.config.get("LANGUAGES", {}).keys()
50
+ backup_lang = session.get(
51
+ "language",
52
+ request.accept_languages.best_match(languages, "en"),
53
+ )
54
+
55
+ if domain_to_lang:
56
+ lang = domain_to_lang.get(domain, backup_lang)
57
+ else:
58
+ lang = backup_lang
59
+
60
+ session["language"] = lang
61
+ return lang
@@ -1,71 +1,20 @@
1
- import os
2
1
  import typing as t
3
2
  import urllib.parse
4
3
 
5
- from flask import Flask, redirect, render_template, request, session
6
- from flask_babel import Babel
4
+ from flask import redirect, render_template, request, session
7
5
  from flask_minify import Minify
8
6
 
9
- from .blog import blog
10
- from .config import (
7
+ from platzky.admin import admin
8
+ from platzky.blog import blog
9
+ from platzky.config import (
11
10
  Config,
12
11
  languages_dict,
13
12
  )
14
- from .db.db_loader import get_db
15
- from .plugin_loader import plugify
16
- from .seo import seo
17
- from .www_handler import redirect_nonwww_to_www, redirect_www_to_nonwww
18
-
19
-
20
- class Engine(Flask):
21
- def __init__(self, config: Config, db, import_name):
22
- super().__init__(import_name)
23
- self.config.from_mapping(config.model_dump(by_alias=True))
24
- self.db = db
25
- self.notifiers = []
26
- self.dynamic_body = ""
27
- self.dynamic_head = ""
28
- directory = os.path.dirname(os.path.realpath(__file__))
29
- locale_dir = os.path.join(directory, "locale")
30
- config.translation_directories.append(locale_dir)
31
-
32
- babel_translation_directories = ";".join(config.translation_directories)
33
- self.babel = Babel(
34
- self,
35
- locale_selector=self.get_locale,
36
- default_translation_directories=babel_translation_directories,
37
- )
38
-
39
- def notify(self, message: str):
40
- for notifier in self.notifiers:
41
- notifier(message)
42
-
43
- def add_notifier(self, notifier):
44
- self.notifiers.append(notifier)
45
-
46
- def add_dynamic_body(self, body: str):
47
- self.dynamic_body += body
48
-
49
- def add_dynamic_head(self, body: str):
50
- self.dynamic_head += body
51
-
52
- def get_locale(self) -> str:
53
- domain = request.headers["Host"]
54
- domain_to_lang = self.config.get("DOMAIN_TO_LANG")
55
-
56
- languages = self.config.get("LANGUAGES", {}).keys()
57
- backup_lang = session.get(
58
- "language",
59
- request.accept_languages.best_match(languages, "en"),
60
- )
61
-
62
- if domain_to_lang:
63
- lang = domain_to_lang.get(domain, backup_lang)
64
- else:
65
- lang = backup_lang
66
-
67
- session["language"] = lang
68
- return lang
13
+ from platzky.db.db_loader import get_db
14
+ from platzky.engine import Engine
15
+ from platzky.plugin.plugin_loader import plugify
16
+ from platzky.seo import seo
17
+ from platzky.www_handler import redirect_nonwww_to_www, redirect_www_to_nonwww
69
18
 
70
19
 
71
20
  def create_engine(config: Config, db) -> Engine:
@@ -132,7 +81,19 @@ def create_engine(config: Config, db) -> Engine:
132
81
 
133
82
 
134
83
  def create_app_from_config(config: Config) -> Engine:
135
- engine = create_engine_from_config(config)
84
+ db = get_db(config.db)
85
+ engine = create_engine(config, db)
86
+
87
+ admin_blueprint = admin.create_admin_blueprint(
88
+ login_methods=engine.login_methods, db=engine.db, locale_func=engine.get_locale
89
+ )
90
+
91
+ if config.feature_flags and config.feature_flags.get("FAKE_LOGIN", False):
92
+ from platzky.admin.fake_login import get_fake_login_html, setup_fake_login_routes
93
+
94
+ engine.login_methods.append(get_fake_login_html())
95
+ admin_blueprint = setup_fake_login_routes(admin_blueprint)
96
+
136
97
  blog_blueprint = blog.create_blog_blueprint(
137
98
  db=engine.db,
138
99
  blog_prefix=config.blog_prefix,
@@ -141,6 +102,7 @@ def create_app_from_config(config: Config) -> Engine:
141
102
  seo_blueprint = seo.create_seo_blueprint(
142
103
  db=engine.db, config=engine.config, locale_func=engine.get_locale
143
104
  )
105
+ engine.register_blueprint(admin_blueprint)
144
106
  engine.register_blueprint(blog_blueprint)
145
107
  engine.register_blueprint(seo_blueprint)
146
108
 
@@ -148,12 +110,6 @@ def create_app_from_config(config: Config) -> Engine:
148
110
  return engine
149
111
 
150
112
 
151
- def create_engine_from_config(config: Config) -> Engine:
152
- """Create an engine from a config."""
153
- db = get_db(config.db)
154
- return create_engine(config, db)
155
-
156
-
157
113
  def create_app(config_path: str) -> Engine:
158
114
  config = Config.parse_yaml(config_path)
159
115
  return create_app_from_config(config)
@@ -0,0 +1,66 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, Dict, Generic, Type, TypeVar
4
+
5
+ from pydantic import BaseModel, ConfigDict
6
+
7
+ from platzky.platzky import Engine as PlatzkyEngine
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class PluginError(Exception):
13
+ """Exception raised for plugin-related errors."""
14
+
15
+ pass
16
+
17
+
18
+ class ConfigPluginError(PluginError):
19
+ """Exception raised for plugin configuration-related errors."""
20
+
21
+ pass
22
+
23
+
24
+ class PluginBaseConfig(BaseModel):
25
+ """Base Pydantic model for plugin configurations.
26
+
27
+ Plugin developers should extend this class to define their own configuration schema.
28
+ """
29
+
30
+ model_config = ConfigDict(extra="allow")
31
+
32
+
33
+ T = TypeVar("T", bound=PluginBaseConfig)
34
+
35
+
36
+ class PluginBase(Generic[T], ABC):
37
+ """Abstract base class for plugins.
38
+
39
+ Plugin developers must extend this class to implement their plugins.
40
+ """
41
+
42
+ @classmethod
43
+ def get_config_model(cls) -> Type[PluginBaseConfig]:
44
+ return PluginBaseConfig
45
+
46
+ def __init__(self, config: Dict[str, Any]):
47
+ try:
48
+ config_class = self.get_config_model()
49
+ self.config = config_class.model_validate(config)
50
+ except Exception as e:
51
+ raise ConfigPluginError(f"Invalid configuration: {e}") from e
52
+
53
+ @abstractmethod
54
+ def process(self, app: PlatzkyEngine) -> PlatzkyEngine:
55
+ """Process the plugin with the given app.
56
+
57
+ Args:
58
+ app: The Flask application instance
59
+
60
+ Returns:
61
+ Platzky Engine with processed plugins
62
+
63
+ Raises:
64
+ PluginError: If plugin processing fails
65
+ """
66
+ pass
@@ -0,0 +1,109 @@
1
+ import importlib.util
2
+ import inspect
3
+ import logging
4
+ from typing import Any, Optional, Type
5
+
6
+ import deprecation
7
+
8
+ from platzky.engine import Engine
9
+ from platzky.plugin.plugin import PluginBase, PluginError
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def find_plugin(plugin_name: str) -> Any:
15
+ """Find plugin by name and return it as module.
16
+
17
+ Args:
18
+ plugin_name: name of plugin to find
19
+
20
+ Raises:
21
+ PluginError: if plugin cannot be imported
22
+
23
+ Returns:
24
+ module of plugin
25
+ """
26
+ try:
27
+ return importlib.import_module(f"platzky_{plugin_name}")
28
+ except ImportError as e:
29
+ raise PluginError(
30
+ f"Plugin {plugin_name} not found. Ensure it's installed and follows "
31
+ f"the 'platzky_<plugin_name>' naming convention"
32
+ ) from e
33
+
34
+
35
+ def _is_class_plugin(plugin_module: Any) -> Optional[Type[PluginBase[Any]]]:
36
+ """Check if the plugin module contains a PluginBase implementation.
37
+
38
+ Args:
39
+ plugin_module: The imported plugin module
40
+
41
+ Returns:
42
+ The plugin class if found, None otherwise
43
+ """
44
+ # Look for classes in the module that inherit from PluginBase
45
+ for _, obj in inspect.getmembers(plugin_module):
46
+ if inspect.isclass(obj) and issubclass(obj, PluginBase) and obj != PluginBase:
47
+ return obj
48
+ return None
49
+
50
+
51
+ @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.",
57
+ )
58
+ def _process_legacy_plugin(plugin_module, app, plugin_config, plugin_name):
59
+ """Process a legacy plugin using the entrypoint approach."""
60
+ app = plugin_module.process(app, plugin_config)
61
+ logger.info(f"Processed legacy plugin: {plugin_name}")
62
+ return app
63
+
64
+
65
+ def plugify(app: Engine) -> Engine:
66
+ """Load plugins and run their entrypoints.
67
+
68
+ Supports both class-based plugins (PluginBase) and legacy entrypoint plugins.
69
+
70
+ Args:
71
+ app: Platzky Engine instance
72
+
73
+ Returns:
74
+ Platzky Engine with processed plugins
75
+
76
+ Raises:
77
+ PluginError: if plugin processing fails
78
+ """
79
+ plugins_data = app.db.get_plugins_data()
80
+
81
+ for plugin_data in plugins_data:
82
+ plugin_config = plugin_data["config"]
83
+ plugin_name = plugin_data["name"]
84
+
85
+ try:
86
+ plugin_module = find_plugin(plugin_name)
87
+
88
+ # Check if this is a class-based plugin
89
+ plugin_class = _is_class_plugin(plugin_module)
90
+
91
+ if plugin_class:
92
+ # Handle new class-based plugins
93
+ plugin_instance = plugin_class(plugin_config)
94
+ app = plugin_instance.process(app)
95
+ logger.info(f"Processed class-based plugin: {plugin_name}")
96
+ elif hasattr(plugin_module, "process"):
97
+ # Handle legacy entrypoint plugins with deprecation warning
98
+ app = _process_legacy_plugin(plugin_module, app, plugin_config, plugin_name)
99
+ else:
100
+ raise PluginError(
101
+ f"Plugin {plugin_name} doesn't implement either the PluginBase interface "
102
+ f"or provide a process() function"
103
+ )
104
+
105
+ except Exception as e:
106
+ logger.error(f"Error processing plugin {plugin_name}: {e}")
107
+ raise PluginError(f"Error processing plugin {plugin_name}: {e}") from e
108
+
109
+ return app
@@ -14,6 +14,17 @@
14
14
  {% block body_meta %}
15
15
  {% include "body_meta.html" %}
16
16
  {% endblock %}
17
+
18
+ {% with messages = get_flashed_messages(with_categories=true) %}
19
+ {% if messages %}
20
+ <div class="flash-messages">
21
+ {% for category, message in messages %}
22
+ <div class="alert alert-{{ category }}">{{ message }}</div>
23
+ {% endfor %}
24
+ </div>
25
+ {% endif %}
26
+ {% endwith %}
27
+
17
28
  <div class="container-fluid d-flex flex-column h-100 g-0">
18
29
  <div class="row header-row bg-light g-0">
19
30
  <nav class="navbar navbar-expand-lg navbar-light px-3 py-1" id="mainNav">
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "platzky"
3
- version = "0.3.0"
3
+ version = "0.3.3"
4
4
  description = "Not only blog engine"
5
5
  authors = []
6
6
  license = "MIT"
@@ -18,6 +18,8 @@ Flask-Minify = "^0.42"
18
18
  google-cloud-storage = "^2.5.0"
19
19
  humanize = "^4.9.0"
20
20
  pydantic = "^2.7.1"
21
+ deprecation = "^2.1.0"
22
+ pygithub = "^2.6.1"
21
23
 
22
24
  [tool.poetry.group.dev.dependencies]
23
25
  pytest = "^8.2.1"
@@ -1,3 +0,0 @@
1
- from .platzky import Engine as Engine
2
- from .platzky import create_app_from_config as create_app_from_config
3
- from .platzky import create_engine as create_engine
@@ -1,43 +0,0 @@
1
- import importlib.util
2
- import logging
3
-
4
- logger = logging.getLogger(__name__)
5
-
6
-
7
- class PluginError(Exception):
8
- pass
9
-
10
-
11
- def find_plugin(plugin_name):
12
- """Find plugin by name and return it as module.
13
- :param plugin_name: name of plugin to find
14
- :raises PluginError: if plugin cannot be imported
15
- :return: module of plugin
16
- """
17
- try:
18
- return importlib.import_module(f"platzky_{plugin_name}")
19
- except ImportError as e:
20
- raise PluginError(
21
- f"Plugin {plugin_name} not found. Ensure it's installed and follows "
22
- f"the 'platzky_<plugin_name>' naming convention"
23
- ) from e
24
-
25
-
26
- def plugify(app):
27
- """Load plugins and run their entrypoints.
28
- :param app: Flask app
29
- :return: Flask app
30
- """
31
-
32
- plugins_data = app.db.get_plugins_data()
33
-
34
- for plugin_data in plugins_data:
35
- plugin_config = plugin_data["config"]
36
- plugin_name = plugin_data["name"]
37
- try:
38
- plugin = find_plugin(plugin_name)
39
- plugin.process(app, plugin_config)
40
- except Exception as e:
41
- raise PluginError(f"Error processing plugin {plugin_name}: {e}") from e
42
-
43
- return app
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes