platzky 0.1.18__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,63 +1,93 @@
1
- from flask import request, render_template, redirect, send_from_directory, make_response, url_for, Blueprint, session, current_app
2
- from platzky.blog import comment_form, post_formatter
1
+ from os.path import dirname
3
2
 
3
+ from flask import Blueprint, make_response, render_template, request
4
+ from markupsafe import Markup
4
5
 
5
- def create_blog_blueprint(db, config, babel):
6
- url_prefix = config["BLOG_PREFIX"]
7
- blog = Blueprint('blog', __name__, url_prefix=url_prefix)
6
+ from . import comment_form
8
7
 
9
- def locale():
10
- 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
+ )
17
+
18
+ @blog.app_template_filter()
19
+ def markdown(text):
20
+ return Markup(text)
11
21
 
12
22
  @blog.errorhandler(404)
13
23
  def page_not_found(e):
14
- return render_template('404.html', title='404'), 404
24
+ return render_template("404.html", title="404"), 404
15
25
 
16
- @blog.route('/', methods=["GET"])
17
- def index():
18
- lang = locale()
19
- 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)
20
34
 
21
- @blog.route('/feed', methods=["GET"])
35
+ @blog.route("/feed", methods=["GET"])
22
36
  def get_feed():
23
- lang = locale()
24
- 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
+ )
25
41
  response.headers["Content-Type"] = "application/xml"
26
42
  return response
27
43
 
28
- @blog.route('/<post_slug>', methods=["GET", "POST"])
44
+ @blog.route("/<post_slug>", methods=["POST"])
45
+ def post_comment(post_slug):
46
+ comment = request.form.to_dict()
47
+ db.add_comment(
48
+ post_slug=post_slug,
49
+ author_name=comment["author_name"],
50
+ comment=comment["comment"],
51
+ )
52
+ return get_post(post_slug=post_slug)
53
+
54
+ @blog.route("/<post_slug>", methods=["GET"])
29
55
  def get_post(post_slug):
30
- if request.method == "POST":
31
- comment = request.form.to_dict()
32
- db.add_comment(post_slug=post_slug, author_name=comment["author_name"],
33
- comment=comment["comment"])
34
- return redirect(url_for('blog.get_post', post_slug=post_slug, comment_sent=True, language=locale()))
35
-
36
- if raw_post := db.get_post(post_slug):
37
- return render_template("post.html", post=post_formatter.format_post(raw_post), post_slug=post_slug,
38
- form=comment_form.CommentForm(), comment_sent=request.args.get('comment_sent'))
39
- else:
40
- return page_not_found("no such page")
41
-
42
- @blog.route('/page/<path:page_slug>', methods=["GET", "POST"])
43
- def get_page(page_slug):
44
- if post := db.get_page(page_slug):
45
- if cover_image := post.get("coverImage"):
46
- cover_image_url = cover_image["url"]
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))
70
+
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
47
79
  else:
48
80
  cover_image_url = None
49
- return render_template("page.html", post=post, cover_image=cover_image_url)
50
- else:
51
- return page_not_found("no such page")
81
+ return render_template("page.html", page=page, cover_image=cover_image_url)
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))
52
86
 
53
- @blog.route('/tag/<path:tag>', methods=["GET"])
87
+ @blog.route("/tag/<path:tag>", methods=["GET"])
54
88
  def get_posts_from_tag(tag):
55
- lang = locale()
89
+ lang = locale_func()
56
90
  posts = db.get_posts_by_tag(tag, lang)
57
91
  return render_template("blog.html", posts=posts, subtitle=f" - tag: {tag}")
58
92
 
59
- @blog.route('/icon/<string:name>', methods=["GET"])
60
- def icon(name):
61
- return send_from_directory('../static/icons', f"{name}.png")
62
-
63
93
  return blog
@@ -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,60 +1,66 @@
1
- import os.path
2
-
1
+ import sys
2
+ import typing as t
3
3
  import yaml
4
+ from pydantic import ConfigDict, BaseModel, Field
5
+
6
+ from .db.db import DBConfig
7
+ from .db.db_loader import get_db_module
8
+
9
+
10
+ class StrictBaseModel(BaseModel):
11
+ model_config = ConfigDict(frozen=True)
12
+
13
+
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")
18
+
19
+
20
+ Languages = dict[str, LanguageConfig]
21
+ LanguagesMapping = t.Mapping[str, t.Mapping[str, str]]
22
+
23
+
24
+ def languages_dict(languages: Languages) -> LanguagesMapping:
25
+ return {name: lang.model_dump() for name, lang in languages.items()}
26
+
27
+
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")
4
43
 
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
+ )
5
58
 
6
- def is_db_ok(mapping):
7
- if 'DB' not in mapping:
8
- raise Exception("DB not set")
9
- if 'TYPE' not in mapping['DB']:
10
- raise Exception("DB type is not set")
11
- if mapping['DB']['TYPE'] not in ['graph_ql', 'json_file', 'google_json']:
12
- raise Exception("DB type is not supported")
13
- return True
14
-
15
-
16
- class Config():
17
- def __init__(self, mapping):
18
- if is_db_ok(mapping):
19
- self.config = mapping
20
- else:
21
- raise Exception("Config is wrong")
22
-
23
- def add_translations_dir(self, absolute_translation_dir):
24
- self.config["BABEL_TRANSLATION_DIRECTORIES"] += ";" + absolute_translation_dir
25
-
26
- def asdict(self):
27
- return self.config
28
-
29
-
30
- def get_config_mapping(base_config):
31
- default_config = {
32
- "USE_WWW": True,
33
- "SEO_PREFIX": "/",
34
- "BLOG_PREFIX": "/",
35
- "LANGUAGES": {},
36
- "DOMAIN_TO_LANG": {},
37
- "PLUGINS": []
38
- }
39
-
40
- config = default_config | base_config
41
- babel_format_dir = ";".join(config.get("TRANSLATION_DIRECTORIES", []))
42
- config["BABEL_TRANSLATION_DIRECTORIES"] = babel_format_dir
43
- return config
44
-
45
-
46
- def from_file(absolute_config_path):
47
- with open(absolute_config_path, "r") as stream:
48
- file_config = yaml.safe_load(stream)
49
- file_config["CONFIG_PATH"] = absolute_config_path
50
- config_from_file = from_mapping(file_config)
51
- config_directory = os.path.dirname(absolute_config_path)
52
- for x in ["locales", "locale", "translations"]:
53
- translation_directory = os.path.join(config_directory, x)
54
- config_from_file.add_translations_dir(translation_directory)
55
- return config_from_file
56
-
57
-
58
- def from_mapping(mapping):
59
- config_dict = get_config_mapping(mapping)
60
- 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")