platzky 0.1.19__py3-none-any.whl → 0.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.
File without changes
platzky/blog/blog.py CHANGED
@@ -1,15 +1,19 @@
1
- from flask import request, render_template,\
2
- make_response, Blueprint, Markup
3
- from platzky.blog import comment_form, post_formatter
4
1
  from os.path import dirname
5
2
 
3
+ from flask import Blueprint, make_response, render_template, request
4
+ from markupsafe import Markup
6
5
 
7
- def create_blog_blueprint(db, config, babel):
8
- url_prefix = config["BLOG_PREFIX"]
9
- blog = Blueprint('blog', __name__, url_prefix=url_prefix, template_folder=f'{dirname(__file__)}/../templates')
6
+ from . import comment_form
10
7
 
11
- def locale():
12
- return babel.locale_selector_func()
8
+
9
+ def create_blog_blueprint(db, blog_prefix: str, locale_func):
10
+ url_prefix = blog_prefix
11
+ blog = Blueprint(
12
+ "blog",
13
+ __name__,
14
+ url_prefix=url_prefix,
15
+ template_folder=f"{dirname(__file__)}/../templates",
16
+ )
13
17
 
14
18
  @blog.app_template_filter()
15
19
  def markdown(text):
@@ -17,50 +21,72 @@ def create_blog_blueprint(db, config, babel):
17
21
 
18
22
  @blog.errorhandler(404)
19
23
  def page_not_found(e):
20
- return render_template('404.html', title='404'), 404
24
+ return render_template("404.html", title="404"), 404
21
25
 
22
- @blog.route('/', methods=["GET"])
23
- def index():
24
- lang = locale()
25
- return render_template("blog.html", posts=db.get_all_posts(lang))
26
+ @blog.route("/", methods=["GET"])
27
+ def all_posts():
28
+ lang = locale_func()
29
+ posts = db.get_all_posts(lang)
30
+ if not posts:
31
+ return page_not_found("no posts")
32
+ posts_sorted = sorted(posts, reverse=True)
33
+ return render_template("blog.html", posts=posts_sorted)
26
34
 
27
- @blog.route('/feed', methods=["GET"])
35
+ @blog.route("/feed", methods=["GET"])
28
36
  def get_feed():
29
- lang = locale()
30
- response = make_response(render_template("feed.xml", posts=db.get_all_posts(lang)))
37
+ lang = locale_func()
38
+ response = make_response(
39
+ render_template("feed.xml", posts=db.get_all_posts(lang))
40
+ )
31
41
  response.headers["Content-Type"] = "application/xml"
32
42
  return response
33
43
 
34
- @blog.route('/<post_slug>', methods=["POST"])
44
+ @blog.route("/<post_slug>", methods=["POST"])
35
45
  def post_comment(post_slug):
36
46
  comment = request.form.to_dict()
37
- db.add_comment(post_slug=post_slug, author_name=comment["author_name"],
38
- comment=comment["comment"])
47
+ db.add_comment(
48
+ post_slug=post_slug,
49
+ author_name=comment["author_name"],
50
+ comment=comment["comment"],
51
+ )
39
52
  return get_post(post_slug=post_slug)
40
53
 
41
- @blog.route('/<post_slug>', methods=["GET"])
54
+ @blog.route("/<post_slug>", methods=["GET"])
42
55
  def get_post(post_slug):
43
- if raw_post := db.get_post(post_slug):
44
- formatted_post = post_formatter.format_post(raw_post)
45
- return render_template("post.html", post=formatted_post, post_slug=post_slug,
46
- form=comment_form.CommentForm(), comment_sent=request.args.get('comment_sent'))
47
- else:
48
- return page_not_found("no such page")
56
+ post = db.get_post(post_slug)
57
+ try:
58
+ post = db.get_post(post_slug)
59
+ return render_template(
60
+ "post.html",
61
+ post=post,
62
+ post_slug=post_slug,
63
+ form=comment_form.CommentForm(),
64
+ comment_sent=request.args.get("comment_sent"),
65
+ )
66
+ except ValueError:
67
+ return page_not_found(f"no post with slug {post_slug}")
68
+ except Exception as e:
69
+ return page_not_found(str(e))
49
70
 
50
- @blog.route('/page/<path:page_slug>', methods=["GET"])
51
- def get_page(page_slug): # TODO refactor to share code with get_post since they are very similar
52
- if page := db.get_page(page_slug):
53
- if cover_image := page.get("coverImage"):
54
- cover_image_url = cover_image["url"]
71
+ @blog.route("/page/<path:page_slug>", methods=["GET"])
72
+ def get_page(
73
+ page_slug,
74
+ ): # TODO refactor to share code with get_post since they are very similar
75
+ try:
76
+ page = db.get_page(page_slug)
77
+ if cover_image := page.coverImage:
78
+ cover_image_url = cover_image.url
55
79
  else:
56
80
  cover_image_url = None
57
81
  return render_template("page.html", page=page, cover_image=cover_image_url)
58
- else:
59
- return page_not_found("no such page")
82
+ except ValueError:
83
+ return page_not_found("no page with slug {page_slug}")
84
+ except Exception as e:
85
+ return page_not_found(str(e))
60
86
 
61
- @blog.route('/tag/<path:tag>', methods=["GET"])
87
+ @blog.route("/tag/<path:tag>", methods=["GET"])
62
88
  def get_posts_from_tag(tag):
63
- lang = locale()
89
+ lang = locale_func()
64
90
  posts = db.get_posts_by_tag(tag, lang)
65
91
  return render_template("blog.html", posts=posts, subtitle=f" - tag: {tag}")
66
92
 
@@ -1,11 +1,15 @@
1
+ from flask_babel import lazy_gettext
1
2
  from flask_wtf import FlaskForm
2
3
  from wtforms import StringField, SubmitField
3
4
  from wtforms.validators import DataRequired
4
5
  from wtforms.widgets import TextArea
5
- from flask_babel import lazy_gettext
6
6
 
7
7
 
8
8
  class CommentForm(FlaskForm):
9
- author_name = StringField(lazy_gettext('Name'), validators=[DataRequired()])
10
- comment = StringField(lazy_gettext('Type comment here'), validators=[DataRequired()], widget=TextArea())
11
- submit = SubmitField(lazy_gettext('Comment'))
9
+ author_name = StringField(str(lazy_gettext("Name")), validators=[DataRequired()])
10
+ comment = StringField(
11
+ str(lazy_gettext("Type comment here")),
12
+ validators=[DataRequired()],
13
+ widget=TextArea(),
14
+ )
15
+ submit = SubmitField(str(lazy_gettext("Comment")))
platzky/config.py CHANGED
@@ -1,63 +1,66 @@
1
- import os.path
1
+ import sys
2
+ import typing as t
2
3
  import yaml
4
+ from pydantic import ConfigDict, BaseModel, Field
3
5
 
6
+ from .db.db import DBConfig
7
+ from .db.db_loader import get_db_module
4
8
 
5
- def is_db_ok(mapping):
6
- if 'DB' not in mapping:
7
- raise Exception("DB not set")
8
- if 'TYPE' not in mapping['DB']:
9
- raise Exception("DB type is not set")
10
- if mapping['DB']['TYPE'] not in ['graph_ql', 'json_file', 'google_json']:
11
- raise Exception("DB type is not supported")
12
- return True
13
9
 
10
+ class StrictBaseModel(BaseModel):
11
+ model_config = ConfigDict(frozen=True)
14
12
 
15
- class Config():
16
- def __init__(self, mapping):
17
- if is_db_ok(mapping):
18
- self.config = mapping
19
- else:
20
- raise Exception("Config is wrong")
21
13
 
22
- def add_translations_dir(self, absolute_translation_dir):
23
- self.config["BABEL_TRANSLATION_DIRECTORIES"] += ";" + absolute_translation_dir
14
+ class LanguageConfig(StrictBaseModel):
15
+ name: str = Field(alias="name")
16
+ flag: str = Field(alias="flag")
17
+ domain: t.Optional[str] = Field(default=None, alias="domain")
24
18
 
25
- def asdict(self):
26
- return self.config
27
19
 
20
+ Languages = dict[str, LanguageConfig]
21
+ LanguagesMapping = t.Mapping[str, t.Mapping[str, str]]
28
22
 
29
- def get_config_mapping(base_config):
30
- default_config = { # TODO move it to platzky
31
- "USE_WWW": True,
32
- "SEO_PREFIX": "/",
33
- "BLOG_PREFIX": "/",
34
- "LANGUAGES": {},
35
- "DOMAIN_TO_LANG": {},
36
- "PLUGINS": []
37
- }
38
23
 
39
- config = default_config | base_config
40
- babel_format_dir = ";".join(config.get("TRANSLATION_DIRECTORIES", []))
41
- config["BABEL_TRANSLATION_DIRECTORIES"] = babel_format_dir
42
- return config
24
+ def languages_dict(languages: Languages) -> LanguagesMapping:
25
+ return {name: lang.model_dump() for name, lang in languages.items()}
43
26
 
44
27
 
45
- def from_file(absolute_config_path):
46
- with open(absolute_config_path, "r") as stream:
47
- file_config = yaml.safe_load(stream)
48
- file_config["CONFIG_PATH"] = absolute_config_path
49
- config_from_file = from_mapping(file_config)
50
- config_directory = os.path.dirname(absolute_config_path)
51
- for x in ["locales", "locale", "translations"]:
52
- translation_directory = os.path.join(config_directory, x)
53
- config_from_file.add_translations_dir(translation_directory)
28
+ class Config(StrictBaseModel):
29
+ app_name: str = Field(alias="APP_NAME")
30
+ secret_key: str = Field(alias="SECRET_KEY")
31
+ db: DBConfig = Field(alias="DB")
32
+ use_www: bool = Field(default=True, alias="USE_WWW")
33
+ seo_prefix: str = Field(default="/", alias="SEO_PREFIX")
34
+ blog_prefix: str = Field(default="/", alias="BLOG_PREFIX")
35
+ languages: Languages = Field(default_factory=dict, alias="LANGUAGES")
36
+ domain_to_lang: dict[str, str] = Field(default_factory=dict, alias="DOMAIN_TO_LANG")
37
+ translation_directories: list[str] = Field(
38
+ default_factory=list,
39
+ alias="TRANSLATION_DIRECTORIES",
40
+ )
41
+ debug: bool = Field(default=False, alias="DEBUG")
42
+ testing: bool = Field(default=False, alias="TESTING")
54
43
 
55
- path_to_module_locale = os.path.join(os.path.dirname(__file__), "./locale")
56
- config_from_file.add_translations_dir(path_to_module_locale)
44
+ @classmethod
45
+ def model_validate(
46
+ cls,
47
+ obj: t.Any,
48
+ *,
49
+ strict: bool | None = None,
50
+ from_attributes: bool | None = None,
51
+ context: dict[str, t.Any] | None = None,
52
+ ) -> "Config":
53
+ db_cfg_type = get_db_module(obj["DB"]["TYPE"]).db_config_type()
54
+ obj["DB"] = db_cfg_type.model_validate(obj["DB"])
55
+ return super().model_validate(
56
+ obj, strict=strict, from_attributes=from_attributes, context=context
57
+ )
57
58
 
58
- return config_from_file
59
-
60
-
61
- def from_mapping(mapping):
62
- config_dict = get_config_mapping(mapping)
63
- return Config(config_dict)
59
+ @classmethod
60
+ def parse_yaml(cls, path: str) -> "Config":
61
+ try:
62
+ with open(path, "r") as f:
63
+ return cls.model_validate(yaml.safe_load(f))
64
+ except FileNotFoundError:
65
+ print(f"Config file not found: {path}", file=sys.stderr)
66
+ raise SystemExit(1)
platzky/db/__init__.py ADDED
File without changes
platzky/db/db.py ADDED
@@ -0,0 +1,107 @@
1
+ from functools import partial
2
+ from typing import Any, Callable
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+ from abc import abstractmethod, ABC
7
+ from ..models import MenuItem, Post, Page, Color
8
+
9
+
10
+ class DB(ABC):
11
+ db_name: str = "DB"
12
+ module_name: str = "db"
13
+ config_type: type
14
+
15
+ def __init_subclass__(cls, *args, **kw):
16
+ """Check that all methods defined in the subclass exist in the superclasses.
17
+ This is to prevent subclasses from adding new methods to the DB object.
18
+ """
19
+ super().__init_subclass__(*args, **kw)
20
+ for name in cls.__dict__:
21
+ if name.startswith("_"):
22
+ continue
23
+ for superclass in cls.__mro__[1:]:
24
+ if name in dir(superclass):
25
+ break
26
+ else:
27
+ raise TypeError(
28
+ f"Method {name} defined in {cls.__name__} does not exist in superclasses"
29
+ )
30
+
31
+ def extend(self, function_name: str, function: Callable) -> None:
32
+ """
33
+ Add a function to the DB object. The function must take the DB object as first parameter.
34
+
35
+ Parameters:
36
+ function_name (str): The name of the function to add.
37
+ function (Callable): The function to add to the DB object.
38
+ """
39
+ if not callable(function):
40
+ raise ValueError(
41
+ f"The provided func for '{function_name}' is not callable."
42
+ )
43
+ try:
44
+ bound_function = partial(function, self)
45
+ setattr(self, function_name, bound_function)
46
+ except Exception as e:
47
+ raise ValueError(f"Failed to extend DB with function {function_name}: {e}")
48
+
49
+ @abstractmethod
50
+ def get_all_posts(self, lang) -> list[Post]:
51
+ pass
52
+
53
+ @abstractmethod
54
+ def get_menu_items(self) -> list[MenuItem]:
55
+ pass
56
+
57
+ @abstractmethod
58
+ def get_post(self, slug) -> Post:
59
+ pass
60
+
61
+ @abstractmethod
62
+ def get_page(self, slug) -> Page:
63
+ pass
64
+
65
+ @abstractmethod
66
+ def get_posts_by_tag(self, tag, lang) -> Any:
67
+ pass
68
+
69
+ @abstractmethod
70
+ def add_comment(self, author_name, comment, post_slug) -> None:
71
+ pass
72
+
73
+ @abstractmethod
74
+ def get_logo_url(self) -> str:
75
+ pass
76
+
77
+ @abstractmethod
78
+ def get_primary_color(self) -> Color:
79
+ pass
80
+
81
+ @abstractmethod
82
+ def get_secondary_color(self) -> Color:
83
+ pass
84
+
85
+ @abstractmethod
86
+ def get_plugins_data(self) -> list:
87
+ pass
88
+
89
+ @abstractmethod
90
+ def get_font(self) -> str:
91
+ pass
92
+
93
+ @abstractmethod
94
+ def get_all_providers(self): # TODO providers are not part of the DB
95
+ pass
96
+
97
+ @abstractmethod
98
+ def get_all_questions(self): # TODO questions are not part of the DB
99
+ pass
100
+
101
+ @abstractmethod
102
+ def get_site_content(self) -> str:
103
+ pass
104
+
105
+
106
+ class DBConfig(BaseModel):
107
+ type: str = Field(alias="TYPE")
@@ -0,0 +1,32 @@
1
+ import importlib.util
2
+ import os
3
+ import sys
4
+ from os.path import abspath, dirname
5
+
6
+
7
+ def get_db(db_config):
8
+ db_name = db_config.type
9
+ db = get_db_module(db_name)
10
+ return db.db_from_config(db_config)
11
+
12
+
13
+ def get_db_module(db_type):
14
+ """
15
+ Load db module from db_type
16
+ This function is used to load db module dynamically as it is specified in config file.
17
+ :param db_type: name of db module
18
+ :return: db module
19
+ """
20
+ db_dir = dirname(abspath(__file__))
21
+ parent_module_name = ".".join(__name__.split(".")[:-1])
22
+ module_name = f"{parent_module_name}.{db_type}_db"
23
+ spec = importlib.util.spec_from_file_location(
24
+ module_name, os.path.join(db_dir, f"{db_type}_db.py")
25
+ )
26
+ assert spec is not None
27
+ db = importlib.util.module_from_spec(spec)
28
+ sys.modules[f"{db_type}_db"] = db
29
+ assert spec.loader is not None
30
+ spec.loader.exec_module(db)
31
+
32
+ return db
@@ -1,17 +1,34 @@
1
1
  import json
2
- from google.cloud import storage
2
+
3
+ from google.cloud.storage import Client
4
+ from pydantic import Field
5
+
6
+ from .db import DBConfig
3
7
  from .json_db import Json
4
8
 
5
9
 
6
- def get_db(app_config):
7
- config = app_config["DB"]
8
- bucket_name = config["BUCKET_NAME"]
9
- source_blob_name = config["SOURCE_BLOB_NAME"]
10
- return GoogleJsonDb(bucket_name, source_blob_name)
10
+ def db_config_type():
11
+ return GoogleJsonDbConfig
12
+
13
+
14
+ class GoogleJsonDbConfig(DBConfig):
15
+ bucket_name: str = Field(alias="BUCKET_NAME")
16
+ source_blob_name: str = Field(alias="SOURCE_BLOB_NAME")
17
+
18
+
19
+ def db_from_config(config: GoogleJsonDbConfig):
20
+ return GoogleJsonDb(config.bucket_name, config.source_blob_name)
21
+
22
+
23
+ def get_db(config):
24
+ google_json_db_config = GoogleJsonDbConfig.model_validate(config)
25
+ return GoogleJsonDb(
26
+ google_json_db_config.bucket_name, google_json_db_config.source_blob_name
27
+ )
11
28
 
12
29
 
13
30
  def get_blob(bucket_name, source_blob_name):
14
- storage_client = storage.Client()
31
+ storage_client = Client()
15
32
  bucket = storage_client.bucket(bucket_name)
16
33
  return bucket.blob(source_blob_name)
17
34
 
@@ -23,11 +40,17 @@ def get_data(blob):
23
40
 
24
41
  class GoogleJsonDb(Json):
25
42
  def __init__(self, bucket_name, source_blob_name):
26
- self.blob = get_blob(bucket_name, source_blob_name)
43
+ self.bucket_name = bucket_name
44
+ self.source_blob_name = source_blob_name
45
+
46
+ self.blob = get_blob(self.bucket_name, self.source_blob_name)
27
47
  data = get_data(self.blob)
28
48
  super().__init__(data)
29
49
 
30
- def save_entry(self, entry):
50
+ self.module_name = "google_json_db"
51
+ self.db_name = "GoogleJsonDb"
52
+
53
+ def __save_entry(self, entry):
31
54
  data = get_data(self.blob)
32
55
  data["data"].append(entry)
33
- self.blob.upload_from_string(json.dumps(data), content_type='application/json')
56
+ self.blob.upload_from_string(json.dumps(data), content_type="application/json")