platzky 1.0.1__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
platzky/admin/admin.py CHANGED
@@ -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(login_methods, cms_modules):
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:
@@ -6,11 +6,12 @@ environments as it bypasses proper authentication and authorization controls.
6
6
  """
7
7
 
8
8
  import os
9
- from typing import Any, Callable
9
+ from collections.abc import Callable
10
10
 
11
11
  from flask import Blueprint, flash, redirect, render_template_string, session, url_for
12
12
  from flask_wtf import FlaskForm
13
13
  from markupsafe import Markup
14
+ from werkzeug.wrappers import Response
14
15
 
15
16
  ROLE_ADMIN = "admin"
16
17
  ROLE_NONADMIN = "nonadmin"
@@ -89,7 +90,7 @@ def setup_fake_login_routes(admin_blueprint: Blueprint) -> Blueprint:
89
90
  )
90
91
 
91
92
  @admin_blueprint.route("/fake-login/<role>", methods=["POST"])
92
- def handle_fake_login(role: str) -> Any:
93
+ def handle_fake_login(role: str) -> Response:
93
94
  form = FakeLoginForm()
94
95
  if form.validate_on_submit() and role in VALID_ROLES:
95
96
  if role == ROLE_ADMIN:
platzky/blog/blog.py CHANGED
@@ -1,12 +1,36 @@
1
+ """Blueprint for blog functionality including posts, pages, and comments."""
2
+
3
+ import logging
4
+ from collections.abc import Callable
1
5
  from os.path import dirname
6
+ from typing import TypeVar
2
7
 
3
- from flask import Blueprint, make_response, render_template, request
8
+ from flask import Blueprint, abort, make_response, render_template, request
4
9
  from markupsafe import Markup
10
+ from werkzeug.exceptions import HTTPException
11
+ from werkzeug.wrappers import Response
12
+
13
+ from platzky.db.db import DB
14
+ from platzky.models import Page, Post
5
15
 
6
16
  from . import comment_form
7
17
 
18
+ ContentType = TypeVar("ContentType", Post, Page)
19
+
20
+ logger = logging.getLogger(__name__)
21
+
8
22
 
9
- 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
+ """
10
34
  url_prefix = blog_prefix
11
35
  blog = Blueprint(
12
36
  "blog",
@@ -16,31 +40,65 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
16
40
  )
17
41
 
18
42
  @blog.app_template_filter()
19
- 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
+ """
20
52
  return Markup(text)
21
53
 
22
54
  @blog.errorhandler(404)
23
- def page_not_found(e):
55
+ def page_not_found(_e: HTTPException) -> tuple[str, int]:
56
+ """Handle 404 Not Found errors in blog routes.
57
+
58
+ Args:
59
+ _e: HTTPException object containing error details (unused)
60
+
61
+ Returns:
62
+ Tuple of rendered 404 template and HTTP 404 status code
63
+ """
24
64
  return render_template("404.html", title="404"), 404
25
65
 
26
66
  @blog.route("/", methods=["GET"])
27
- def all_posts():
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
+ """
28
73
  lang = locale_func()
29
74
  posts = db.get_all_posts(lang)
30
75
  if not posts:
31
- return page_not_found("no posts")
76
+ abort(404)
32
77
  posts_sorted = sorted(posts, reverse=True)
33
78
  return render_template("blog.html", posts=posts_sorted)
34
79
 
35
80
  @blog.route("/feed", methods=["GET"])
36
- def get_feed():
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
+ """
37
87
  lang = locale_func()
38
88
  response = make_response(render_template("feed.xml", posts=db.get_all_posts(lang)))
39
89
  response.headers["Content-Type"] = "application/xml"
40
90
  return response
41
91
 
42
92
  @blog.route("/<post_slug>", methods=["POST"])
43
- def post_comment(post_slug):
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
+ """
44
102
  comment = request.form.to_dict()
45
103
  db.add_comment(
46
104
  post_slug=post_slug,
@@ -49,40 +107,71 @@ def create_blog_blueprint(db, blog_prefix: str, locale_func):
49
107
  )
50
108
  return get_post(post_slug=post_slug)
51
109
 
52
- @blog.route("/<post_slug>", methods=["GET"])
53
- def get_post(post_slug):
110
+ def _get_content_or_404(
111
+ getter_func: Callable[[str], ContentType],
112
+ slug: str,
113
+ ) -> ContentType:
114
+ """Helper to fetch content from database or abort with 404.
115
+
116
+ Args:
117
+ getter_func: Database getter function (e.g., db.get_post, db.get_page)
118
+ slug: Content slug to fetch
119
+
120
+ Returns:
121
+ The fetched content object
122
+
123
+ Raises:
124
+ HTTPException: 404 if content not found
125
+ """
54
126
  try:
55
- post = db.get_post(post_slug)
56
- return render_template(
57
- "post.html",
58
- post=post,
59
- post_slug=post_slug,
60
- form=comment_form.CommentForm(),
61
- comment_sent=request.args.get("comment_sent"),
62
- )
63
- except ValueError:
64
- return page_not_found(f"no post with slug {post_slug}")
65
- except Exception as e:
66
- return page_not_found(str(e))
127
+ return getter_func(slug)
128
+ except ValueError as e:
129
+ logger.debug("Content not found for slug '%s': %s", slug, e)
130
+ abort(404)
131
+
132
+ @blog.route("/<post_slug>", methods=["GET"])
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
+ """
142
+ post = _get_content_or_404(db.get_post, post_slug)
143
+ return render_template(
144
+ "post.html",
145
+ post=post,
146
+ post_slug=post_slug,
147
+ form=comment_form.CommentForm(),
148
+ comment_sent=request.args.get("comment_sent"),
149
+ )
67
150
 
68
151
  @blog.route("/page/<path:page_slug>", methods=["GET"])
69
- def get_page(
70
- page_slug,
71
- ): # TODO refactor to share code with get_post since they are very similar
72
- try:
73
- page = db.get_page(page_slug)
74
- if cover_image := page.coverImage:
75
- cover_image_url = cover_image.url
76
- else:
77
- cover_image_url = None
78
- return render_template("page.html", page=page, cover_image=cover_image_url)
79
- except ValueError:
80
- return page_not_found("no page with slug {page_slug}")
81
- except Exception as e:
82
- return page_not_found(str(e))
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
+ """
161
+ page = _get_content_or_404(db.get_page, page_slug)
162
+ cover_image_url = page.coverImage.url if page.coverImage.url else None
163
+ return render_template("page.html", page=page, cover_image=cover_image_url)
83
164
 
84
165
  @blog.route("/tag/<path:tag>", methods=["GET"])
85
- def get_posts_from_tag(tag):
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
+ """
86
175
  lang = locale_func()
87
176
  posts = db.get_posts_by_tag(tag, lang)
88
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")),
platzky/config.py CHANGED
@@ -165,7 +165,7 @@ class Config(StrictBaseModel):
165
165
  @classmethod
166
166
  def model_validate(
167
167
  cls,
168
- obj: t.Any,
168
+ obj: dict[str, t.Any],
169
169
  *,
170
170
  strict: bool | None = None,
171
171
  from_attributes: bool | None = None,
platzky/db/db.py CHANGED
@@ -1,6 +1,9 @@
1
+ """Abstract base classes for database implementations."""
2
+
1
3
  from abc import ABC, abstractmethod
4
+ from collections.abc import Callable
2
5
  from functools import partial
3
- from typing import Any, Callable
6
+ from typing import Any
4
7
 
5
8
  from pydantic import BaseModel, Field
6
9
 
@@ -8,6 +11,8 @@ from platzky.models import MenuItem, Page, Post
8
11
 
9
12
 
10
13
  class DB(ABC):
14
+ """Abstract base class for all database implementations."""
15
+
11
16
  db_name: str = "DB"
12
17
  module_name: str = "db"
13
18
  config_type: type
@@ -45,55 +50,99 @@ class DB(ABC):
45
50
  raise ValueError(f"Failed to extend DB with function {function_name}: {e}")
46
51
 
47
52
  @abstractmethod
48
- 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
+ """
49
59
  pass
50
60
 
51
61
  @abstractmethod
52
- 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
+ """
53
68
  pass
54
69
 
55
70
  @abstractmethod
56
- 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
+ """
57
77
  pass
58
78
 
59
79
  @abstractmethod
60
- 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
+ """
61
86
  pass
62
87
 
63
88
  @abstractmethod
64
- 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
+ """
65
95
  pass
66
96
 
67
97
  @abstractmethod
68
- def get_posts_by_tag(self, tag, lang) -> Any:
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
+ """
69
105
  pass
70
106
 
71
107
  @abstractmethod
72
- 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
+ """
73
116
  pass
74
117
 
75
118
  @abstractmethod
76
- def get_logo_url(self) -> str: # TODO provide alternative text along with the URL of logo
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."""
77
121
  pass
78
122
 
79
123
  @abstractmethod
80
124
  def get_favicon_url(self) -> str:
125
+ """Retrieve the URL of the application favicon."""
81
126
  pass
82
127
 
83
128
  @abstractmethod
84
129
  def get_primary_color(self) -> str:
130
+ """Retrieve the primary color for the application theme."""
85
131
  pass
86
132
 
87
133
  @abstractmethod
88
134
  def get_secondary_color(self) -> str:
135
+ """Retrieve the secondary color for the application theme."""
89
136
  pass
90
137
 
91
138
  @abstractmethod
92
- def get_plugins_data(self) -> list[Any]:
139
+ def get_plugins_data(self) -> list[dict[str, Any]]:
140
+ """Retrieve configuration data for all plugins."""
93
141
  pass
94
142
 
95
143
  @abstractmethod
96
144
  def get_font(self) -> str:
145
+ """Get the font configuration for the application."""
97
146
  pass
98
147
 
99
148
  @abstractmethod
@@ -107,4 +156,6 @@ class DB(ABC):
107
156
 
108
157
 
109
158
  class DBConfig(BaseModel):
159
+ """Base configuration class for database connections."""
160
+
110
161
  type: str = Field(alias="TYPE")
platzky/db/db_loader.py CHANGED
@@ -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
- def get_db(db_config):
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
- def __init__(self, github_token: str, repo_name: str, branch_name: str, path_to_file: str):
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
@@ -1,4 +1,7 @@
1
+ """Google Cloud Storage-based JSON database implementation."""
2
+
1
3
  import json
4
+ from typing import TYPE_CHECKING, Any
2
5
 
3
6
  from google.cloud.storage import Client
4
7
  from pydantic import Field
@@ -6,38 +9,91 @@ from pydantic import Field
6
9
  from platzky.db.db import DBConfig
7
10
  from platzky.db.json_db import Json
8
11
 
12
+ if TYPE_CHECKING:
13
+ from google.cloud.storage import Blob
14
+
9
15
 
10
- def db_config_type():
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
+ """
11
22
  return GoogleJsonDbConfig
12
23
 
13
24
 
14
25
  class GoogleJsonDbConfig(DBConfig):
26
+ """Configuration for Google Cloud Storage JSON database connection."""
27
+
15
28
  bucket_name: str = Field(alias="BUCKET_NAME")
16
29
  source_blob_name: str = Field(alias="SOURCE_BLOB_NAME")
17
30
 
18
31
 
19
- def db_from_config(config: GoogleJsonDbConfig):
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
+ """
20
41
  return GoogleJsonDb(config.bucket_name, config.source_blob_name)
21
42
 
22
43
 
23
- def get_db(config):
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
+ """
24
53
  google_json_db_config = GoogleJsonDbConfig.model_validate(config)
25
54
  return GoogleJsonDb(google_json_db_config.bucket_name, google_json_db_config.source_blob_name)
26
55
 
27
56
 
28
- def get_blob(bucket_name, source_blob_name):
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
+ """
29
67
  storage_client = Client()
30
68
  bucket = storage_client.bucket(bucket_name)
31
69
  return bucket.blob(source_blob_name)
32
70
 
33
71
 
34
- def get_data(blob):
35
- raw_data = blob.download_as_text(client=None)
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
36
84
  return json.loads(raw_data)
37
85
 
38
86
 
39
87
  class GoogleJsonDb(Json):
40
- def __init__(self, bucket_name, source_blob_name):
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
+ """
41
97
  self.bucket_name = bucket_name
42
98
  self.source_blob_name = source_blob_name
43
99