platzky 1.1.0__tar.gz → 1.2.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-1.1.0 → platzky-1.2.1}/PKG-INFO +1 -1
- {platzky-1.1.0 → platzky-1.2.1}/platzky/admin/admin.py +25 -3
- {platzky-1.1.0 → platzky-1.2.1}/platzky/admin/fake_login.py +2 -2
- {platzky-1.1.0 → platzky-1.2.1}/platzky/blog/blog.py +72 -5
- {platzky-1.1.0 → platzky-1.2.1}/platzky/blog/comment_form.py +10 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/config.py +1 -1
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/db.py +59 -9
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/db_loader.py +5 -2
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/github_json_db.py +42 -4
- platzky-1.2.1/platzky/db/google_json_db.py +105 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/graph_ql_db.py +216 -31
- platzky-1.2.1/platzky/db/json_db.py +260 -0
- platzky-1.2.1/platzky/db/json_file_db.py +81 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/mongodb_db.py +134 -6
- {platzky-1.1.0 → platzky-1.2.1}/platzky/engine.py +4 -3
- {platzky-1.1.0 → platzky-1.2.1}/platzky/models.py +19 -9
- {platzky-1.1.0 → platzky-1.2.1}/platzky/plugin/plugin_loader.py +4 -3
- platzky-1.2.1/platzky/seo/seo.py +108 -0
- {platzky-1.1.0 → platzky-1.2.1}/pyproject.toml +7 -9
- platzky-1.1.0/platzky/db/google_json_db.py +0 -49
- platzky-1.1.0/platzky/db/json_db.py +0 -134
- platzky-1.1.0/platzky/db/json_file_db.py +0 -41
- platzky-1.1.0/platzky/seo/seo.py +0 -72
- {platzky-1.1.0 → platzky-1.2.1}/LICENSE +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/README.md +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/__init__.py +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/admin/templates/admin.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/admin/templates/login.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/admin/templates/module.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/blog/__init__.py +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/README.md +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/db/__init__.py +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/locale/en/LC_MESSAGES/messages.po +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/locale/pl/LC_MESSAGES/messages.po +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/platzky.py +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/plugin/plugin.py +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/static/blog.css +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/static/styles.css +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/telemetry.py +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/404.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/base.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/blog.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/body_meta.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/dynamic_css.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/feed.xml +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/head_meta.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/page.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/post.html +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/robots.txt +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/templates/sitemap.xml +0 -0
- {platzky-1.1.0 → platzky-1.2.1}/platzky/www_handler.py +0 -0
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
"""Blueprint for admin panel functionality."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
1
4
|
from os.path import dirname
|
|
2
5
|
|
|
3
6
|
from flask import Blueprint, render_template, session
|
|
4
7
|
|
|
8
|
+
from platzky.models import CmsModule
|
|
9
|
+
|
|
5
10
|
|
|
6
|
-
def create_admin_blueprint(
|
|
11
|
+
def create_admin_blueprint(
|
|
12
|
+
login_methods: list[Callable[[], str]], cms_modules: list[CmsModule]
|
|
13
|
+
) -> Blueprint:
|
|
7
14
|
"""Create admin blueprint with dynamic module routes.
|
|
8
15
|
|
|
9
16
|
Args:
|
|
10
17
|
login_methods: Available login methods
|
|
11
18
|
cms_modules: List of CMS modules to register routes for
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Configured Flask Blueprint for admin panel
|
|
12
22
|
"""
|
|
13
23
|
# …rest of the function…
|
|
14
24
|
admin = Blueprint(
|
|
@@ -21,12 +31,24 @@ def create_admin_blueprint(login_methods, cms_modules):
|
|
|
21
31
|
for module in cms_modules:
|
|
22
32
|
|
|
23
33
|
@admin.route(f"/module/{module.slug}", methods=["GET"])
|
|
24
|
-
def module_route(module=module):
|
|
34
|
+
def module_route(module: CmsModule = module) -> str:
|
|
35
|
+
"""Render a CMS module page.
|
|
25
36
|
|
|
37
|
+
Args:
|
|
38
|
+
module: CMS module object containing template and configuration
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Rendered HTML template for the module
|
|
42
|
+
"""
|
|
26
43
|
return render_template(module.template, module=module)
|
|
27
44
|
|
|
28
45
|
@admin.route("/", methods=["GET"])
|
|
29
|
-
def admin_panel_home():
|
|
46
|
+
def admin_panel_home() -> str:
|
|
47
|
+
"""Display admin panel home or login page.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Rendered login page if not authenticated, admin panel if authenticated
|
|
51
|
+
"""
|
|
30
52
|
user = session.get("user", None)
|
|
31
53
|
|
|
32
54
|
if not user:
|
|
@@ -7,11 +7,11 @@ environments as it bypasses proper authentication and authorization controls.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
from collections.abc import Callable
|
|
10
|
-
from typing import Any
|
|
11
10
|
|
|
12
11
|
from flask import Blueprint, flash, redirect, render_template_string, session, url_for
|
|
13
12
|
from flask_wtf import FlaskForm
|
|
14
13
|
from markupsafe import Markup
|
|
14
|
+
from werkzeug.wrappers import Response
|
|
15
15
|
|
|
16
16
|
ROLE_ADMIN = "admin"
|
|
17
17
|
ROLE_NONADMIN = "nonadmin"
|
|
@@ -90,7 +90,7 @@ def setup_fake_login_routes(admin_blueprint: Blueprint) -> Blueprint:
|
|
|
90
90
|
)
|
|
91
91
|
|
|
92
92
|
@admin_blueprint.route("/fake-login/<role>", methods=["POST"])
|
|
93
|
-
def handle_fake_login(role: str) ->
|
|
93
|
+
def handle_fake_login(role: str) -> Response:
|
|
94
94
|
form = FakeLoginForm()
|
|
95
95
|
if form.validate_on_submit() and role in VALID_ROLES:
|
|
96
96
|
if role == ROLE_ADMIN:
|
|
@@ -1,19 +1,36 @@
|
|
|
1
|
+
"""Blueprint for blog functionality including posts, pages, and comments."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
from collections.abc import Callable
|
|
3
5
|
from os.path import dirname
|
|
4
|
-
from typing import
|
|
6
|
+
from typing import TypeVar
|
|
5
7
|
|
|
6
8
|
from flask import Blueprint, abort, make_response, render_template, request
|
|
7
9
|
from markupsafe import Markup
|
|
8
10
|
from werkzeug.exceptions import HTTPException
|
|
9
11
|
from werkzeug.wrappers import Response
|
|
10
12
|
|
|
13
|
+
from platzky.db.db import DB
|
|
14
|
+
from platzky.models import Page, Post
|
|
15
|
+
|
|
11
16
|
from . import comment_form
|
|
12
17
|
|
|
18
|
+
ContentType = TypeVar("ContentType", Post, Page)
|
|
19
|
+
|
|
13
20
|
logger = logging.getLogger(__name__)
|
|
14
21
|
|
|
15
22
|
|
|
16
|
-
def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
23
|
+
def create_blog_blueprint(db: DB, blog_prefix: str, locale_func: Callable[[], str]) -> Blueprint:
|
|
24
|
+
"""Create and configure the blog blueprint with all routes and handlers.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
db: Database instance for accessing blog content
|
|
28
|
+
blog_prefix: URL prefix for blog routes
|
|
29
|
+
locale_func: Function that returns the current locale/language code
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Configured Flask Blueprint for blog functionality
|
|
33
|
+
"""
|
|
17
34
|
url_prefix = blog_prefix
|
|
18
35
|
blog = Blueprint(
|
|
19
36
|
"blog",
|
|
@@ -23,7 +40,15 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
23
40
|
)
|
|
24
41
|
|
|
25
42
|
@blog.app_template_filter()
|
|
26
|
-
def markdown(text):
|
|
43
|
+
def markdown(text: str) -> Markup:
|
|
44
|
+
"""Template filter to render markdown text as safe HTML.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
text: Markdown text to be rendered
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Markup object containing safe HTML
|
|
51
|
+
"""
|
|
27
52
|
return Markup(text)
|
|
28
53
|
|
|
29
54
|
@blog.errorhandler(404)
|
|
@@ -40,6 +65,11 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
40
65
|
|
|
41
66
|
@blog.route("/", methods=["GET"])
|
|
42
67
|
def all_posts() -> str:
|
|
68
|
+
"""Display all blog posts for the current language.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Rendered HTML template with all blog posts
|
|
72
|
+
"""
|
|
43
73
|
lang = locale_func()
|
|
44
74
|
posts = db.get_all_posts(lang)
|
|
45
75
|
if not posts:
|
|
@@ -49,6 +79,11 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
49
79
|
|
|
50
80
|
@blog.route("/feed", methods=["GET"])
|
|
51
81
|
def get_feed() -> Response:
|
|
82
|
+
"""Generate RSS/Atom feed for blog posts.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
XML response containing the RSS/Atom feed
|
|
86
|
+
"""
|
|
52
87
|
lang = locale_func()
|
|
53
88
|
response = make_response(render_template("feed.xml", posts=db.get_all_posts(lang)))
|
|
54
89
|
response.headers["Content-Type"] = "application/xml"
|
|
@@ -56,6 +91,14 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
56
91
|
|
|
57
92
|
@blog.route("/<post_slug>", methods=["POST"])
|
|
58
93
|
def post_comment(post_slug: str) -> str:
|
|
94
|
+
"""Handle comment submission for a blog post.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
post_slug: URL slug of the blog post
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Rendered HTML template of the blog post with new comment
|
|
101
|
+
"""
|
|
59
102
|
comment = request.form.to_dict()
|
|
60
103
|
db.add_comment(
|
|
61
104
|
post_slug=post_slug,
|
|
@@ -65,9 +108,9 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
65
108
|
return get_post(post_slug=post_slug)
|
|
66
109
|
|
|
67
110
|
def _get_content_or_404(
|
|
68
|
-
getter_func: Callable[[str],
|
|
111
|
+
getter_func: Callable[[str], ContentType],
|
|
69
112
|
slug: str,
|
|
70
|
-
) ->
|
|
113
|
+
) -> ContentType:
|
|
71
114
|
"""Helper to fetch content from database or abort with 404.
|
|
72
115
|
|
|
73
116
|
Args:
|
|
@@ -88,6 +131,14 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
88
131
|
|
|
89
132
|
@blog.route("/<post_slug>", methods=["GET"])
|
|
90
133
|
def get_post(post_slug: str) -> str:
|
|
134
|
+
"""Display a single blog post with comments.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
post_slug: URL slug of the blog post
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Rendered HTML template of the blog post
|
|
141
|
+
"""
|
|
91
142
|
post = _get_content_or_404(db.get_post, post_slug)
|
|
92
143
|
return render_template(
|
|
93
144
|
"post.html",
|
|
@@ -99,12 +150,28 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
|
|
|
99
150
|
|
|
100
151
|
@blog.route("/page/<path:page_slug>", methods=["GET"])
|
|
101
152
|
def get_page(page_slug: str) -> str:
|
|
153
|
+
"""Display a static page.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
page_slug: URL slug of the page
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Rendered HTML template of the page
|
|
160
|
+
"""
|
|
102
161
|
page = _get_content_or_404(db.get_page, page_slug)
|
|
103
162
|
cover_image_url = page.coverImage.url if page.coverImage.url else None
|
|
104
163
|
return render_template("page.html", page=page, cover_image=cover_image_url)
|
|
105
164
|
|
|
106
165
|
@blog.route("/tag/<path:tag>", methods=["GET"])
|
|
107
166
|
def get_posts_from_tag(tag: str) -> str:
|
|
167
|
+
"""Display all blog posts with a specific tag.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
tag: Tag name to filter posts by
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Rendered HTML template with filtered blog posts
|
|
174
|
+
"""
|
|
108
175
|
lang = locale_func()
|
|
109
176
|
posts = db.get_posts_by_tag(tag, lang)
|
|
110
177
|
return render_template("blog.html", posts=posts, subtitle=f" - tag: {tag}")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Form for blog post comments."""
|
|
2
|
+
|
|
1
3
|
from flask_babel import lazy_gettext
|
|
2
4
|
from flask_wtf import FlaskForm
|
|
3
5
|
from wtforms import StringField, SubmitField
|
|
@@ -6,6 +8,14 @@ from wtforms.widgets import TextArea
|
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
class CommentForm(FlaskForm):
|
|
11
|
+
"""Form for submitting comments on blog posts.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
author_name: Required text field for the commenter's name.
|
|
15
|
+
comment: Required text area for the comment content.
|
|
16
|
+
submit: Submit button to post the comment.
|
|
17
|
+
"""
|
|
18
|
+
|
|
9
19
|
author_name = StringField(str(lazy_gettext("Name")), validators=[DataRequired()])
|
|
10
20
|
comment = StringField(
|
|
11
21
|
str(lazy_gettext("Type comment here")),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Abstract base classes for database implementations."""
|
|
2
|
+
|
|
1
3
|
from abc import ABC, abstractmethod
|
|
2
4
|
from collections.abc import Callable
|
|
3
5
|
from functools import partial
|
|
@@ -9,6 +11,8 @@ from platzky.models import MenuItem, Page, Post
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class DB(ABC):
|
|
14
|
+
"""Abstract base class for all database implementations."""
|
|
15
|
+
|
|
12
16
|
db_name: str = "DB"
|
|
13
17
|
module_name: str = "db"
|
|
14
18
|
config_type: type
|
|
@@ -46,55 +50,99 @@ class DB(ABC):
|
|
|
46
50
|
raise ValueError(f"Failed to extend DB with function {function_name}: {e}")
|
|
47
51
|
|
|
48
52
|
@abstractmethod
|
|
49
|
-
def get_app_description(self, lang) -> str:
|
|
53
|
+
def get_app_description(self, lang: str) -> str:
|
|
54
|
+
"""Retrieve the application description for a specific language.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
58
|
+
"""
|
|
50
59
|
pass
|
|
51
60
|
|
|
52
61
|
@abstractmethod
|
|
53
|
-
def get_all_posts(self, lang) -> list[Post]:
|
|
62
|
+
def get_all_posts(self, lang: str) -> list[Post]:
|
|
63
|
+
"""Retrieve all posts for a specific language.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
67
|
+
"""
|
|
54
68
|
pass
|
|
55
69
|
|
|
56
70
|
@abstractmethod
|
|
57
|
-
def get_menu_items_in_lang(self, lang) -> list[MenuItem]:
|
|
71
|
+
def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
|
|
72
|
+
"""Retrieve menu items for a specific language.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
76
|
+
"""
|
|
58
77
|
pass
|
|
59
78
|
|
|
60
79
|
@abstractmethod
|
|
61
|
-
def get_post(self, slug) -> Post:
|
|
80
|
+
def get_post(self, slug: str) -> Post:
|
|
81
|
+
"""Retrieve a single post by its slug.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
slug: URL-friendly identifier for the post
|
|
85
|
+
"""
|
|
62
86
|
pass
|
|
63
87
|
|
|
64
88
|
@abstractmethod
|
|
65
|
-
def get_page(self, slug) -> Page:
|
|
89
|
+
def get_page(self, slug: str) -> Page:
|
|
90
|
+
"""Retrieve a page by its slug.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
slug: URL-friendly identifier for the page
|
|
94
|
+
"""
|
|
66
95
|
pass
|
|
67
96
|
|
|
68
97
|
@abstractmethod
|
|
69
|
-
def get_posts_by_tag(self, tag, lang) ->
|
|
98
|
+
def get_posts_by_tag(self, tag: str, lang: str) -> list[Post]:
|
|
99
|
+
"""Retrieve posts filtered by tag and language.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
tag: Tag name to filter by
|
|
103
|
+
lang: Language code (e.g., 'en', 'pl')
|
|
104
|
+
"""
|
|
70
105
|
pass
|
|
71
106
|
|
|
72
107
|
@abstractmethod
|
|
73
|
-
def add_comment(self, author_name, comment, post_slug) -> None:
|
|
108
|
+
def add_comment(self, author_name: str, comment: str, post_slug: str) -> None:
|
|
109
|
+
"""Add a new comment to a post.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
author_name: Name of the comment author
|
|
113
|
+
comment: Comment text content
|
|
114
|
+
post_slug: URL-friendly identifier of the post
|
|
115
|
+
"""
|
|
74
116
|
pass
|
|
75
117
|
|
|
76
118
|
@abstractmethod
|
|
77
|
-
def get_logo_url(self) -> str: # TODO
|
|
119
|
+
def get_logo_url(self) -> str: # TODO: Provide alternative text along with the URL of logo
|
|
120
|
+
"""Retrieve the URL of the application logo."""
|
|
78
121
|
pass
|
|
79
122
|
|
|
80
123
|
@abstractmethod
|
|
81
124
|
def get_favicon_url(self) -> str:
|
|
125
|
+
"""Retrieve the URL of the application favicon."""
|
|
82
126
|
pass
|
|
83
127
|
|
|
84
128
|
@abstractmethod
|
|
85
129
|
def get_primary_color(self) -> str:
|
|
130
|
+
"""Retrieve the primary color for the application theme."""
|
|
86
131
|
pass
|
|
87
132
|
|
|
88
133
|
@abstractmethod
|
|
89
134
|
def get_secondary_color(self) -> str:
|
|
135
|
+
"""Retrieve the secondary color for the application theme."""
|
|
90
136
|
pass
|
|
91
137
|
|
|
92
138
|
@abstractmethod
|
|
93
|
-
def get_plugins_data(self) -> list[Any]:
|
|
139
|
+
def get_plugins_data(self) -> list[dict[str, Any]]:
|
|
140
|
+
"""Retrieve configuration data for all plugins."""
|
|
94
141
|
pass
|
|
95
142
|
|
|
96
143
|
@abstractmethod
|
|
97
144
|
def get_font(self) -> str:
|
|
145
|
+
"""Get the font configuration for the application."""
|
|
98
146
|
pass
|
|
99
147
|
|
|
100
148
|
@abstractmethod
|
|
@@ -108,4 +156,6 @@ class DB(ABC):
|
|
|
108
156
|
|
|
109
157
|
|
|
110
158
|
class DBConfig(BaseModel):
|
|
159
|
+
"""Base configuration class for database connections."""
|
|
160
|
+
|
|
111
161
|
type: str = Field(alias="TYPE")
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import importlib.util
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
+
import types
|
|
4
5
|
from os.path import abspath, dirname
|
|
5
6
|
|
|
7
|
+
from platzky.db.db import DB, DBConfig
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
def get_db(db_config: DBConfig) -> DB:
|
|
8
11
|
db_name = db_config.type
|
|
9
12
|
db = get_db_module(db_name)
|
|
10
13
|
return db.db_from_config(db_config)
|
|
11
14
|
|
|
12
15
|
|
|
13
|
-
def get_db_module(db_type):
|
|
16
|
+
def get_db_module(db_type: str) -> types.ModuleType:
|
|
14
17
|
"""
|
|
15
18
|
Load db module from db_type
|
|
16
19
|
This function is used to load db module dynamically as it is specified in config file.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
"""GitHub-based JSON database implementation."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
4
|
+
from typing import Any
|
|
2
5
|
|
|
3
6
|
import requests
|
|
4
7
|
from github import Github
|
|
@@ -8,24 +11,47 @@ from platzky.db.db import DBConfig
|
|
|
8
11
|
from platzky.db.json_db import Json as JsonDB
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
def db_config_type():
|
|
14
|
+
def db_config_type() -> type["GithubJsonDbConfig"]:
|
|
15
|
+
"""Return the configuration class for GitHub JSON database.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
GithubJsonDbConfig class
|
|
19
|
+
"""
|
|
12
20
|
return GithubJsonDbConfig
|
|
13
21
|
|
|
14
22
|
|
|
15
23
|
class GithubJsonDbConfig(DBConfig):
|
|
24
|
+
"""Configuration for GitHub JSON database connection."""
|
|
25
|
+
|
|
16
26
|
github_token: str = Field(alias="GITHUB_TOKEN")
|
|
17
27
|
repo_name: str = Field(alias="REPO_NAME")
|
|
18
28
|
path_to_file: str = Field(alias="PATH_TO_FILE")
|
|
19
29
|
branch_name: str = Field(alias="BRANCH_NAME", default="main")
|
|
20
30
|
|
|
21
31
|
|
|
22
|
-
def db_from_config(config: GithubJsonDbConfig):
|
|
32
|
+
def db_from_config(config: GithubJsonDbConfig) -> "GithubJsonDb":
|
|
33
|
+
"""Create a GitHub JSON database instance from configuration.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: GitHub JSON database configuration
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Configured GitHub JSON database instance
|
|
40
|
+
"""
|
|
23
41
|
return GithubJsonDb(
|
|
24
42
|
config.github_token, config.repo_name, config.branch_name, config.path_to_file
|
|
25
43
|
)
|
|
26
44
|
|
|
27
45
|
|
|
28
|
-
def get_db(config):
|
|
46
|
+
def get_db(config: dict[str, Any]) -> "GithubJsonDb":
|
|
47
|
+
"""Get a GitHub JSON database instance from raw configuration.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config: Raw configuration dictionary
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Configured GitHub JSON database instance
|
|
54
|
+
"""
|
|
29
55
|
github_json_db_config = GithubJsonDbConfig.model_validate(config)
|
|
30
56
|
return GithubJsonDb(
|
|
31
57
|
github_json_db_config.github_token,
|
|
@@ -36,7 +62,19 @@ def get_db(config):
|
|
|
36
62
|
|
|
37
63
|
|
|
38
64
|
class GithubJsonDb(JsonDB):
|
|
39
|
-
|
|
65
|
+
"""JSON database stored in a GitHub repository."""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self, github_token: str, repo_name: str, branch_name: str, path_to_file: str
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Initialize GitHub JSON database connection.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
github_token: GitHub personal access token
|
|
74
|
+
repo_name: Full repository name (e.g., 'owner/repo')
|
|
75
|
+
branch_name: Branch name to read from
|
|
76
|
+
path_to_file: Path to the JSON file within the repository
|
|
77
|
+
"""
|
|
40
78
|
self.branch_name = branch_name
|
|
41
79
|
self.repo = Github(github_token).get_repo(repo_name)
|
|
42
80
|
self.file_path = path_to_file
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Google Cloud Storage-based JSON database implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from google.cloud.storage import Client
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from platzky.db.db import DBConfig
|
|
10
|
+
from platzky.db.json_db import Json
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from google.cloud.storage import Blob
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def db_config_type() -> type["GoogleJsonDbConfig"]:
|
|
17
|
+
"""Return the configuration class for Google Cloud Storage JSON database.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
GoogleJsonDbConfig class
|
|
21
|
+
"""
|
|
22
|
+
return GoogleJsonDbConfig
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class GoogleJsonDbConfig(DBConfig):
|
|
26
|
+
"""Configuration for Google Cloud Storage JSON database connection."""
|
|
27
|
+
|
|
28
|
+
bucket_name: str = Field(alias="BUCKET_NAME")
|
|
29
|
+
source_blob_name: str = Field(alias="SOURCE_BLOB_NAME")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def db_from_config(config: GoogleJsonDbConfig) -> "GoogleJsonDb":
|
|
33
|
+
"""Create a Google Cloud Storage JSON database instance from configuration.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: Google Cloud Storage JSON database configuration
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Configured Google Cloud Storage JSON database instance
|
|
40
|
+
"""
|
|
41
|
+
return GoogleJsonDb(config.bucket_name, config.source_blob_name)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_db(config: dict[str, Any]) -> "GoogleJsonDb":
|
|
45
|
+
"""Get a Google Cloud Storage JSON database instance from raw configuration.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: Raw configuration dictionary
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Configured Google Cloud Storage JSON database instance
|
|
52
|
+
"""
|
|
53
|
+
google_json_db_config = GoogleJsonDbConfig.model_validate(config)
|
|
54
|
+
return GoogleJsonDb(google_json_db_config.bucket_name, google_json_db_config.source_blob_name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_blob(bucket_name: str, source_blob_name: str) -> "Blob":
|
|
58
|
+
"""Retrieve a blob from Google Cloud Storage.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
bucket_name: Name of the GCS bucket
|
|
62
|
+
source_blob_name: Name of the blob/file in the bucket
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
GCS Blob object
|
|
66
|
+
"""
|
|
67
|
+
storage_client = Client()
|
|
68
|
+
bucket = storage_client.bucket(bucket_name)
|
|
69
|
+
return bucket.blob(source_blob_name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_data(blob: "Blob") -> dict[str, Any]:
|
|
73
|
+
"""Download and parse JSON data from a blob.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
blob: GCS Blob object to download from
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Parsed JSON data as dictionary
|
|
80
|
+
"""
|
|
81
|
+
raw_data = (
|
|
82
|
+
blob.download_as_text()
|
|
83
|
+
) # pyright: ignore[reportCallIssue] - Incomplete type stubs for google.cloud.storage Blob
|
|
84
|
+
return json.loads(raw_data)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class GoogleJsonDb(Json):
|
|
88
|
+
"""JSON database stored in Google Cloud Storage."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, bucket_name: str, source_blob_name: str) -> None:
|
|
91
|
+
"""Initialize Google Cloud Storage JSON database connection.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
bucket_name: Name of the GCS bucket
|
|
95
|
+
source_blob_name: Name of the blob/file in the bucket
|
|
96
|
+
"""
|
|
97
|
+
self.bucket_name = bucket_name
|
|
98
|
+
self.source_blob_name = source_blob_name
|
|
99
|
+
|
|
100
|
+
self.blob = get_blob(self.bucket_name, self.source_blob_name)
|
|
101
|
+
data = get_data(self.blob)
|
|
102
|
+
super().__init__(data)
|
|
103
|
+
|
|
104
|
+
self.module_name = "google_json_db"
|
|
105
|
+
self.db_name = "GoogleJsonDb"
|