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.
- {platzky-0.3.0 → platzky-0.3.3}/PKG-INFO +3 -1
- platzky-0.3.3/platzky/__init__.py +3 -0
- platzky-0.3.3/platzky/admin/admin.py +33 -0
- platzky-0.3.3/platzky/admin/fake_login.py +82 -0
- platzky-0.3.3/platzky/admin/templates/admin.html +30 -0
- platzky-0.3.3/platzky/admin/templates/login.html +23 -0
- platzky-0.3.3/platzky/admin/templates/module.html +9 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/blog/blog.py +0 -1
- platzky-0.3.3/platzky/db/README.md +70 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/db.py +1 -1
- platzky-0.3.3/platzky/db/github_json_db.py +69 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/google_json_db.py +2 -7
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/graph_ql_db.py +4 -4
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/json_db.py +7 -3
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/json_file_db.py +2 -2
- platzky-0.3.3/platzky/engine.py +61 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/platzky.py +23 -67
- platzky-0.3.3/platzky/plugin/plugin.py +66 -0
- platzky-0.3.3/platzky/plugin/plugin_loader.py +109 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/base.html +11 -0
- {platzky-0.3.0 → platzky-0.3.3}/pyproject.toml +3 -1
- platzky-0.3.0/platzky/__init__.py +0 -3
- platzky-0.3.0/platzky/plugin_loader.py +0 -43
- {platzky-0.3.0 → platzky-0.3.3}/README.md +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/blog/__init__.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/blog/comment_form.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/config.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/__init__.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/db/db_loader.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/locale/en/LC_MESSAGES/messages.po +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/locale/pl/LC_MESSAGES/messages.po +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/models.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/seo/seo.py +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/static/blog.css +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/static/styles.css +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/404.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/blog.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/body_meta.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/dynamic_css.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/feed.xml +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/head_meta.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/page.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/post.html +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/robots.txt +0 -0
- {platzky-0.3.0 → platzky-0.3.3}/platzky/templates/sitemap.xml +0 -0
- {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.
|
|
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
|

|
|
@@ -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,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
|
+
```
|
|
@@ -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
|
|
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"],
|
|
@@ -3,8 +3,8 @@ from typing import Any, Dict
|
|
|
3
3
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from .
|
|
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 (
|
|
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,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
|
|
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:
|
|
@@ -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
|
-
|
|
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.
|
|
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,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
|
|
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
|