platzky 0.2.18__tar.gz → 0.3.1__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.
- {platzky-0.2.18 → platzky-0.3.1}/PKG-INFO +2 -2
- platzky-0.3.1/platzky/__init__.py +3 -0
- platzky-0.3.1/platzky/admin/admin.py +33 -0
- platzky-0.3.1/platzky/admin/templates/admin.html +29 -0
- platzky-0.3.1/platzky/admin/templates/login.html +21 -0
- platzky-0.3.1/platzky/admin/templates/module.html +9 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/db.py +1 -1
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/google_json_db.py +0 -5
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/graph_ql_db.py +4 -4
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/json_db.py +5 -1
- platzky-0.3.1/platzky/engine.py +62 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/platzky.py +13 -60
- platzky-0.3.1/platzky/plugin/plugin.py +66 -0
- platzky-0.3.1/platzky/plugin/plugin_loader.py +109 -0
- {platzky-0.2.18 → platzky-0.3.1}/pyproject.toml +4 -4
- platzky-0.2.18/platzky/__init__.py +0 -3
- platzky-0.2.18/platzky/plugin_loader.py +0 -81
- platzky-0.2.18/platzky/plugins/google-tag-manager/entrypoint.py +0 -30
- platzky-0.2.18/platzky/plugins/redirections/entrypoint.py +0 -64
- {platzky-0.2.18 → platzky-0.3.1}/README.md +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/blog/__init__.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/blog/blog.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/blog/comment_form.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/config.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/__init__.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/db_loader.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/db/json_file_db.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/locale/en/LC_MESSAGES/messages.po +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/locale/pl/LC_MESSAGES/messages.po +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/models.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/seo/seo.py +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/static/blog.css +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/static/styles.css +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/404.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/base.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/blog.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/body_meta.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/dynamic_css.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/feed.xml +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/head_meta.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/page.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/post.html +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/robots.txt +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/templates/sitemap.xml +0 -0
- {platzky-0.2.18 → platzky-0.3.1}/platzky/www_handler.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: platzky
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Not only blog engine
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10,<4.0
|
|
@@ -16,10 +16,10 @@ 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
|
-
Requires-Dist: platzky-sendmail (>=0.1.1,<0.2.0)
|
|
23
23
|
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
|
|
@@ -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,29 @@
|
|
|
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
|
+
{% for cms_module_name, cms_entries in cms_modules.items() %}
|
|
21
|
+
<div class="cms-module mb-2">
|
|
22
|
+
<p>{{ cms_module_name }}</p>
|
|
23
|
+
{% for cms_entry in cms_entries %}
|
|
24
|
+
<a href="{{ url_for('admin.module', name=cms_entry) }}" class="cms-entry btn btn-primary btn-block mb-2">{{ cms_entry }}</a>
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</div>
|
|
27
|
+
{% endfor %}
|
|
28
|
+
</div>
|
|
29
|
+
{% endblock %}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 login_method in login_methods %}
|
|
16
|
+
{{ login_method | safe }}
|
|
17
|
+
{% endfor %}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
{% endblock %}
|
|
@@ -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
|
|
10
|
-
from .
|
|
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
|
-
):
|
|
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):
|
|
40
|
+
def _standarize_post(post):
|
|
41
41
|
return {
|
|
42
42
|
"author": post["author"]["name"],
|
|
43
43
|
"slug": post["slug"],
|
|
@@ -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 (
|
|
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")
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
|
|
22
|
+
babel_translation_directories = ";".join(config.translation_directories)
|
|
23
|
+
self.babel = Babel(
|
|
24
|
+
self,
|
|
25
|
+
locale_selector=self.get_locale,
|
|
26
|
+
default_translation_directories=babel_translation_directories,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def notify(self, message: str):
|
|
30
|
+
for notifier in self.notifiers:
|
|
31
|
+
notifier(message)
|
|
32
|
+
|
|
33
|
+
def add_notifier(self, notifier):
|
|
34
|
+
self.notifiers.append(notifier)
|
|
35
|
+
|
|
36
|
+
# TODO login_method should be interface
|
|
37
|
+
def add_login_method(self, login_method):
|
|
38
|
+
self.login_methods.append(login_method)
|
|
39
|
+
|
|
40
|
+
def add_dynamic_body(self, body: str):
|
|
41
|
+
self.dynamic_body += body
|
|
42
|
+
|
|
43
|
+
def add_dynamic_head(self, body: str):
|
|
44
|
+
self.dynamic_head += body
|
|
45
|
+
|
|
46
|
+
def get_locale(self) -> str:
|
|
47
|
+
domain = request.headers.get("Host", "localhost")
|
|
48
|
+
domain_to_lang = self.config.get("DOMAIN_TO_LANG")
|
|
49
|
+
|
|
50
|
+
languages = self.config.get("LANGUAGES", {}).keys()
|
|
51
|
+
backup_lang = session.get(
|
|
52
|
+
"language",
|
|
53
|
+
request.accept_languages.best_match(languages, "en"),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if domain_to_lang:
|
|
57
|
+
lang = domain_to_lang.get(domain, backup_lang)
|
|
58
|
+
else:
|
|
59
|
+
lang = backup_lang
|
|
60
|
+
|
|
61
|
+
session["language"] = lang
|
|
62
|
+
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
|
|
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 .
|
|
10
|
-
from .
|
|
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 .
|
|
16
|
-
from .
|
|
17
|
-
from .
|
|
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:
|
|
@@ -133,6 +82,9 @@ def create_engine(config: Config, db) -> Engine:
|
|
|
133
82
|
|
|
134
83
|
def create_app_from_config(config: Config) -> Engine:
|
|
135
84
|
engine = create_engine_from_config(config)
|
|
85
|
+
admin_blueprint = admin.create_admin_blueprint(
|
|
86
|
+
login_methods=engine.login_methods, db=engine.db, locale_func=engine.get_locale
|
|
87
|
+
)
|
|
136
88
|
blog_blueprint = blog.create_blog_blueprint(
|
|
137
89
|
db=engine.db,
|
|
138
90
|
blog_prefix=config.blog_prefix,
|
|
@@ -141,6 +93,7 @@ def create_app_from_config(config: Config) -> Engine:
|
|
|
141
93
|
seo_blueprint = seo.create_seo_blueprint(
|
|
142
94
|
db=engine.db, config=engine.config, locale_func=engine.get_locale
|
|
143
95
|
)
|
|
96
|
+
engine.register_blueprint(admin_blueprint)
|
|
144
97
|
engine.register_blueprint(blog_blueprint)
|
|
145
98
|
engine.register_blueprint(seo_blueprint)
|
|
146
99
|
|
|
@@ -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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "platzky"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Not only blog engine"
|
|
5
5
|
authors = []
|
|
6
6
|
license = "MIT"
|
|
@@ -18,7 +18,7 @@ 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
|
-
|
|
21
|
+
deprecation = "^2.1.0"
|
|
22
22
|
|
|
23
23
|
[tool.poetry.group.dev.dependencies]
|
|
24
24
|
pytest = "^8.2.1"
|
|
@@ -73,14 +73,14 @@ target-version = ["py310"]
|
|
|
73
73
|
line-length = 100
|
|
74
74
|
target-version = "py310"
|
|
75
75
|
show-fixes = true
|
|
76
|
-
select = [
|
|
76
|
+
lint.select = [
|
|
77
77
|
"I", # isort
|
|
78
78
|
"F", # Pyflakes
|
|
79
79
|
"E", # pycodestyle Error
|
|
80
80
|
"W", # pycodestyle Warning
|
|
81
81
|
"RUF", # Ruff-specific rules
|
|
82
82
|
]
|
|
83
|
-
ignore = []
|
|
83
|
+
lint.ignore = []
|
|
84
84
|
|
|
85
85
|
[tool.pytest.ini_options]
|
|
86
86
|
markers = [
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import importlib.util
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import sys
|
|
5
|
-
from os.path import abspath, dirname
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class PluginError(Exception):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# TODO remove find_local_plugin after all plugins will be extracted
|
|
15
|
-
def find_local_plugin(plugin_name):
|
|
16
|
-
"""Find plugin by name and return it as module.
|
|
17
|
-
:param plugin_name: name of plugin to find
|
|
18
|
-
:return: module of plugin
|
|
19
|
-
"""
|
|
20
|
-
plugins_dir = os.path.join(dirname(abspath(__file__)), "plugins")
|
|
21
|
-
module_name = plugin_name.removesuffix(".py")
|
|
22
|
-
spec = importlib.util.spec_from_file_location(
|
|
23
|
-
module_name, os.path.join(plugins_dir, plugin_name, "entrypoint.py")
|
|
24
|
-
)
|
|
25
|
-
assert spec is not None
|
|
26
|
-
plugin = importlib.util.module_from_spec(spec)
|
|
27
|
-
sys.modules[module_name] = plugin
|
|
28
|
-
assert spec.loader is not None
|
|
29
|
-
spec.loader.exec_module(plugin)
|
|
30
|
-
return plugin
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def find_installed_plugin(plugin_name):
|
|
34
|
-
"""Find plugin by name and return it as module.
|
|
35
|
-
:param plugin_name: name of plugin to find
|
|
36
|
-
:raises PluginError: if plugin cannot be imported
|
|
37
|
-
:return: module of plugin
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
return importlib.import_module(f"platzky_{plugin_name}")
|
|
41
|
-
except ImportError as e:
|
|
42
|
-
raise PluginError(
|
|
43
|
-
f"Plugin {plugin_name} not found. Ensure it's installed and follows "
|
|
44
|
-
f"the 'platzky_<plugin_name>' naming convention"
|
|
45
|
-
) from e
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def find_plugin(plugin_name):
|
|
49
|
-
"""Find plugin by name and return it as module.
|
|
50
|
-
:param plugin_name: name of plugin to find
|
|
51
|
-
:raises PluginError: if plugin cannot be found or imported
|
|
52
|
-
:return: module of plugin
|
|
53
|
-
"""
|
|
54
|
-
plugin = None
|
|
55
|
-
try:
|
|
56
|
-
plugin = find_local_plugin(plugin_name)
|
|
57
|
-
except FileNotFoundError:
|
|
58
|
-
logger.info(f"Local plugin {plugin_name} not found, trying installed version")
|
|
59
|
-
plugin = find_installed_plugin(plugin_name)
|
|
60
|
-
|
|
61
|
-
return plugin
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def plugify(app):
|
|
65
|
-
"""Load plugins and run their entrypoints.
|
|
66
|
-
:param app: Flask app
|
|
67
|
-
:return: Flask app
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
plugins_data = app.db.get_plugins_data()
|
|
71
|
-
|
|
72
|
-
for plugin_data in plugins_data:
|
|
73
|
-
plugin_config = plugin_data["config"]
|
|
74
|
-
plugin_name = plugin_data["name"]
|
|
75
|
-
try:
|
|
76
|
-
plugin = find_plugin(plugin_name)
|
|
77
|
-
plugin.process(app, plugin_config)
|
|
78
|
-
except Exception as e:
|
|
79
|
-
raise PluginError(f"Error processing plugin {plugin_name}: {e}") from e
|
|
80
|
-
|
|
81
|
-
return app
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
def process(app, plugin_config):
|
|
2
|
-
gtm_id = plugin_config["ID"]
|
|
3
|
-
|
|
4
|
-
head_code = (
|
|
5
|
-
"""<!-- Google Tag Manager -->
|
|
6
|
-
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
|
7
|
-
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
|
8
|
-
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
|
9
|
-
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
10
|
-
})(window,document,'script','dataLayer','"""
|
|
11
|
-
+ gtm_id
|
|
12
|
-
+ """');</script>
|
|
13
|
-
<!-- End Google Tag Manager -->
|
|
14
|
-
"""
|
|
15
|
-
)
|
|
16
|
-
app.add_dynamic_head(head_code)
|
|
17
|
-
|
|
18
|
-
body = (
|
|
19
|
-
"""<!-- Google Tag Manager (noscript) -->
|
|
20
|
-
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id="""
|
|
21
|
-
+ gtm_id
|
|
22
|
-
+ """
|
|
23
|
-
"
|
|
24
|
-
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
|
25
|
-
<!-- End Google Tag Manager (noscript) -->
|
|
26
|
-
"""
|
|
27
|
-
)
|
|
28
|
-
app.add_dynamic_body(body)
|
|
29
|
-
|
|
30
|
-
return app
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
from flask import redirect
|
|
2
|
-
from gql import gql
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def json_db_get_redirections(self):
|
|
7
|
-
return self.data.get("redirections", {})
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def json_file_db_get_redirections(self):
|
|
11
|
-
return json_db_get_redirections(self)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def google_json_db_get_redirections(self):
|
|
15
|
-
return self.data.get("redirections", {})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def graph_ql_db_get_redirections(self):
|
|
19
|
-
redirections = gql(
|
|
20
|
-
"""
|
|
21
|
-
query MyQuery{
|
|
22
|
-
redirections(stage: PUBLISHED){
|
|
23
|
-
source
|
|
24
|
-
destination
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
"""
|
|
28
|
-
)
|
|
29
|
-
return {
|
|
30
|
-
x["source"]: x["destination"] for x in self.client.execute(redirections)["redirections"]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class Redirection(BaseModel):
|
|
35
|
-
source: str
|
|
36
|
-
destiny: str
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def parse_redirections(config: dict[str, str]) -> list[Redirection]:
|
|
40
|
-
return [Redirection(source=source, destiny=destiny) for source, destiny in config.items()]
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def setup_routes(app, redirections):
|
|
44
|
-
for redirection in redirections:
|
|
45
|
-
func = redirect_with_name(
|
|
46
|
-
redirection.destiny,
|
|
47
|
-
code=301,
|
|
48
|
-
name=f"{redirection.source}-{redirection.destiny}",
|
|
49
|
-
)
|
|
50
|
-
app.route(rule=redirection.source)(func)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def redirect_with_name(destiny, code, name):
|
|
54
|
-
def named_redirect(*args, **kwargs):
|
|
55
|
-
return redirect(destiny, code, *args, **kwargs)
|
|
56
|
-
|
|
57
|
-
named_redirect.__name__ = name
|
|
58
|
-
return named_redirect
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def process(app, config: dict[str, str]) -> object:
|
|
62
|
-
redirections = parse_redirections(config)
|
|
63
|
-
setup_routes(app, redirections)
|
|
64
|
-
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|