platzky 0.1.18__tar.gz → 0.2.0__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.2.0/PKG-INFO +39 -0
- platzky-0.2.0/README.md +16 -0
- platzky-0.2.0/platzky/blog/__init__.py +0 -0
- platzky-0.2.0/platzky/blog/blog.py +93 -0
- platzky-0.2.0/platzky/blog/comment_form.py +15 -0
- platzky-0.2.0/platzky/config.py +66 -0
- platzky-0.2.0/platzky/db/__init__.py +0 -0
- platzky-0.2.0/platzky/db/db.py +107 -0
- platzky-0.2.0/platzky/db/db_loader.py +32 -0
- platzky-0.2.0/platzky/db/google_json_db.py +56 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/db/graph_ql_db.py +117 -27
- platzky-0.2.0/platzky/db/json_db.py +110 -0
- platzky-0.2.0/platzky/db/json_file_db.py +41 -0
- platzky-0.2.0/platzky/models.py +64 -0
- platzky-0.2.0/platzky/platzky.py +154 -0
- platzky-0.2.0/platzky/plugin_loader.py +39 -0
- platzky-0.2.0/platzky/plugins/redirections/entrypoint.py +68 -0
- platzky-0.2.0/platzky/plugins/sendmail/entrypoint.py +43 -0
- platzky-0.2.0/platzky/seo/seo.py +77 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/static/blog.css +5 -12
- platzky-0.2.0/platzky/templates/base.html +167 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/blog.html +3 -2
- platzky-0.2.0/platzky/templates/body_meta.html +7 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/feed.xml +5 -5
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/head_meta.html +5 -15
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/page.html +2 -2
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/post.html +2 -2
- {platzky-0.1.18 → platzky-0.2.0}/platzky/www_handler.py +7 -4
- platzky-0.2.0/pyproject.toml +47 -0
- platzky-0.1.18/PKG-INFO +0 -39
- platzky-0.1.18/README.md +0 -16
- platzky-0.1.18/platzky/blog/blog.py +0 -63
- platzky-0.1.18/platzky/blog/comment_form.py +0 -11
- platzky-0.1.18/platzky/blog/db.py +0 -18
- platzky-0.1.18/platzky/blog/post_formatter.py +0 -16
- platzky-0.1.18/platzky/config.py +0 -60
- platzky-0.1.18/platzky/db/google_json_db.py +0 -33
- platzky-0.1.18/platzky/db/json_db.py +0 -46
- platzky-0.1.18/platzky/db/json_file_db.py +0 -25
- platzky-0.1.18/platzky/db_loader.py +0 -25
- platzky-0.1.18/platzky/platzky.py +0 -93
- platzky-0.1.18/platzky/plugin_loader.py +0 -42
- platzky-0.1.18/platzky/plugins/redirections/entrypoint.py +0 -46
- platzky-0.1.18/platzky/plugins/sendmail/entrypoint.py +0 -22
- platzky-0.1.18/platzky/seo/seo.py +0 -66
- platzky-0.1.18/platzky/templates/base.html +0 -52
- platzky-0.1.18/platzky/templates/body_meta.html +0 -24
- platzky-0.1.18/pyproject.toml +0 -28
- platzky-0.1.18/setup.py +0 -44
- {platzky-0.1.18 → platzky-0.2.0}/platzky/__init__.py +0 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/404.html +0 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/home.html +0 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/robots.txt +0 -0
- {platzky-0.1.18 → platzky-0.2.0}/platzky/templates/sitemap.xml +0 -0
platzky-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: platzky
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Not only blog engine
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.10,<4.0
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: Flask (==3.0.3)
|
|
13
|
+
Requires-Dist: Flask-Babel (>=4.0.0,<5.0.0)
|
|
14
|
+
Requires-Dist: Flask-Minify (>=0.42,<0.43)
|
|
15
|
+
Requires-Dist: Flask-WTF (>=1.2.1,<2.0.0)
|
|
16
|
+
Requires-Dist: PyYAML (>=6.0,<7.0)
|
|
17
|
+
Requires-Dist: aiohttp (>=3.9.5,<4.0.0)
|
|
18
|
+
Requires-Dist: google-cloud-storage (>=2.5.0,<3.0.0)
|
|
19
|
+
Requires-Dist: gql (>=3.4.0,<4.0.0)
|
|
20
|
+
Requires-Dist: humanize (>=4.9.0,<5.0.0)
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
[](https://coveralls.io/github/platzky/platzky?branch=main)
|
|
25
|
+
|
|
26
|
+
# platzky
|
|
27
|
+
|
|
28
|
+
Platzky is engine which aims to provide simple and easy way to create and run web applications in python.
|
|
29
|
+
|
|
30
|
+
# How to use?
|
|
31
|
+
|
|
32
|
+
1. Install platzky with your favorite dependency management tool (`pip install platzky` or `poetry add platzky`).
|
|
33
|
+
2. Copy `config-template.yml` to your project directory and fill it with your data.
|
|
34
|
+
3. Run `flask --app "platzky.platzky:create_app(config_path='PATH_TO_YOUR_CONFIG_FILE')`
|
|
35
|
+
|
|
36
|
+
## Example
|
|
37
|
+
|
|
38
|
+
For examples check e2e tests in `tests/e2e` directory and Makefile.
|
|
39
|
+
|
platzky-0.2.0/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+

|
|
2
|
+
[](https://coveralls.io/github/platzky/platzky?branch=main)
|
|
3
|
+
|
|
4
|
+
# platzky
|
|
5
|
+
|
|
6
|
+
Platzky is engine which aims to provide simple and easy way to create and run web applications in python.
|
|
7
|
+
|
|
8
|
+
# How to use?
|
|
9
|
+
|
|
10
|
+
1. Install platzky with your favorite dependency management tool (`pip install platzky` or `poetry add platzky`).
|
|
11
|
+
2. Copy `config-template.yml` to your project directory and fill it with your data.
|
|
12
|
+
3. Run `flask --app "platzky.platzky:create_app(config_path='PATH_TO_YOUR_CONFIG_FILE')`
|
|
13
|
+
|
|
14
|
+
## Example
|
|
15
|
+
|
|
16
|
+
For examples check e2e tests in `tests/e2e` directory and Makefile.
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from os.path import dirname
|
|
2
|
+
|
|
3
|
+
from flask import Blueprint, make_response, render_template, request
|
|
4
|
+
from markupsafe import Markup
|
|
5
|
+
|
|
6
|
+
from . import comment_form
|
|
7
|
+
|
|
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)
|
|
21
|
+
|
|
22
|
+
@blog.errorhandler(404)
|
|
23
|
+
def page_not_found(e):
|
|
24
|
+
return render_template("404.html", title="404"), 404
|
|
25
|
+
|
|
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)
|
|
34
|
+
|
|
35
|
+
@blog.route("/feed", methods=["GET"])
|
|
36
|
+
def get_feed():
|
|
37
|
+
lang = locale_func()
|
|
38
|
+
response = make_response(
|
|
39
|
+
render_template("feed.xml", posts=db.get_all_posts(lang))
|
|
40
|
+
)
|
|
41
|
+
response.headers["Content-Type"] = "application/xml"
|
|
42
|
+
return response
|
|
43
|
+
|
|
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"])
|
|
55
|
+
def get_post(post_slug):
|
|
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
|
|
79
|
+
else:
|
|
80
|
+
cover_image_url = None
|
|
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))
|
|
86
|
+
|
|
87
|
+
@blog.route("/tag/<path:tag>", methods=["GET"])
|
|
88
|
+
def get_posts_from_tag(tag):
|
|
89
|
+
lang = locale_func()
|
|
90
|
+
posts = db.get_posts_by_tag(tag, lang)
|
|
91
|
+
return render_template("blog.html", posts=posts, subtitle=f" - tag: {tag}")
|
|
92
|
+
|
|
93
|
+
return blog
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from flask_babel import lazy_gettext
|
|
2
|
+
from flask_wtf import FlaskForm
|
|
3
|
+
from wtforms import StringField, SubmitField
|
|
4
|
+
from wtforms.validators import DataRequired
|
|
5
|
+
from wtforms.widgets import TextArea
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommentForm(FlaskForm):
|
|
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")))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import typing as t
|
|
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")
|
|
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
|
+
)
|
|
58
|
+
|
|
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)
|
|
File without changes
|
|
@@ -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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from google.cloud.storage import Client
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from .db import DBConfig
|
|
7
|
+
from .json_db import Json
|
|
8
|
+
|
|
9
|
+
|
|
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
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_blob(bucket_name, source_blob_name):
|
|
31
|
+
storage_client = Client()
|
|
32
|
+
bucket = storage_client.bucket(bucket_name)
|
|
33
|
+
return bucket.blob(source_blob_name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_data(blob):
|
|
37
|
+
raw_data = blob.download_as_text(client=None)
|
|
38
|
+
return json.loads(raw_data)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GoogleJsonDb(Json):
|
|
42
|
+
def __init__(self, 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)
|
|
47
|
+
data = get_data(self.blob)
|
|
48
|
+
super().__init__(data)
|
|
49
|
+
|
|
50
|
+
self.module_name = "google_json_db"
|
|
51
|
+
self.db_name = "GoogleJsonDb"
|
|
52
|
+
|
|
53
|
+
def __save_entry(self, entry):
|
|
54
|
+
data = get_data(self.blob)
|
|
55
|
+
data["data"].append(entry)
|
|
56
|
+
self.blob.upload_from_string(json.dumps(data), content_type="application/json")
|
|
@@ -1,22 +1,59 @@
|
|
|
1
|
-
#TODO rename file, extract it to another library, remove qgl and aiohttp from dependencies
|
|
1
|
+
# TODO rename file, extract it to another library, remove qgl and aiohttp from dependencies
|
|
2
2
|
|
|
3
|
-
from gql import gql, Client
|
|
4
|
-
from gql.transport.aiohttp import AIOHTTPTransport
|
|
5
3
|
import json
|
|
6
|
-
from platzky.blog.db import DB
|
|
7
4
|
|
|
5
|
+
from gql import Client, gql
|
|
6
|
+
from gql.transport.aiohttp import AIOHTTPTransport
|
|
7
|
+
from pydantic import Field
|
|
8
|
+
|
|
9
|
+
from .db import DB, DBConfig
|
|
10
|
+
from ..models import Color, Post
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def db_config_type():
|
|
14
|
+
return GraphQlDbConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GraphQlDbConfig(DBConfig):
|
|
18
|
+
endpoint: str = Field(alias="CMS_ENDPOINT")
|
|
19
|
+
token: str = Field(alias="CMS_TOKEN")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_db(config: GraphQlDbConfig):
|
|
23
|
+
return GraphQL(config.endpoint, config.token)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def db_from_config(config: GraphQlDbConfig):
|
|
27
|
+
return GraphQL(config.endpoint, config.token)
|
|
8
28
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
29
|
+
|
|
30
|
+
def _standarize_post(post):
|
|
31
|
+
return {
|
|
32
|
+
"author": post["author"]["name"],
|
|
33
|
+
"slug": post["slug"],
|
|
34
|
+
"title": post["title"],
|
|
35
|
+
"excerpt": post["excerpt"],
|
|
36
|
+
"contentInMarkdown": post["contentInRichText"]["html"],
|
|
37
|
+
"comments": post["comments"],
|
|
38
|
+
"tags": post["tags"],
|
|
39
|
+
"language": post["language"],
|
|
40
|
+
"coverImage": {
|
|
41
|
+
"url": post["coverImage"]["image"]["url"],
|
|
42
|
+
},
|
|
43
|
+
"date": post["date"],
|
|
44
|
+
}
|
|
13
45
|
|
|
14
46
|
|
|
15
47
|
class GraphQL(DB):
|
|
16
48
|
def __init__(self, endpoint, token):
|
|
17
|
-
|
|
18
|
-
|
|
49
|
+
self.module_name = "graph_ql_db"
|
|
50
|
+
self.db_name = "GraphQLDb"
|
|
51
|
+
full_token = "bearer " + token
|
|
52
|
+
transport = AIOHTTPTransport(
|
|
53
|
+
url=endpoint, headers={"Authorization": full_token}
|
|
54
|
+
)
|
|
19
55
|
self.client = Client(transport=transport)
|
|
56
|
+
super().__init__()
|
|
20
57
|
|
|
21
58
|
def get_all_posts(self, lang):
|
|
22
59
|
all_posts = gql(
|
|
@@ -24,22 +61,38 @@ class GraphQL(DB):
|
|
|
24
61
|
query MyQuery($lang: Lang!) {
|
|
25
62
|
posts(where: {language: $lang}, orderBy: date_DESC, stage: PUBLISHED){
|
|
26
63
|
createdAt
|
|
64
|
+
author {
|
|
65
|
+
name
|
|
66
|
+
}
|
|
67
|
+
contentInRichText {
|
|
68
|
+
html
|
|
69
|
+
}
|
|
70
|
+
comments {
|
|
71
|
+
comment
|
|
72
|
+
author
|
|
73
|
+
createdAt
|
|
74
|
+
}
|
|
27
75
|
date
|
|
28
76
|
title
|
|
29
77
|
excerpt
|
|
30
78
|
slug
|
|
31
79
|
tags
|
|
80
|
+
language
|
|
32
81
|
coverImage {
|
|
33
82
|
alternateText
|
|
34
83
|
image {
|
|
35
84
|
url
|
|
36
85
|
}
|
|
37
|
-
}
|
|
86
|
+
}
|
|
38
87
|
}
|
|
39
88
|
}
|
|
40
89
|
"""
|
|
41
90
|
)
|
|
42
|
-
|
|
91
|
+
raw_ql_posts = self.client.execute(all_posts, variable_values={"lang": lang})[
|
|
92
|
+
"posts"
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
return [Post.model_validate(_standarize_post(post)) for post in raw_ql_posts]
|
|
43
96
|
|
|
44
97
|
def get_menu_items(self):
|
|
45
98
|
menu_items = gql(
|
|
@@ -52,17 +105,23 @@ class GraphQL(DB):
|
|
|
52
105
|
}
|
|
53
106
|
"""
|
|
54
107
|
)
|
|
55
|
-
return self.client.execute(menu_items)[
|
|
108
|
+
return self.client.execute(menu_items)["menuItems"]
|
|
56
109
|
|
|
57
110
|
def get_post(self, slug):
|
|
58
111
|
post = gql(
|
|
59
112
|
"""
|
|
60
113
|
query MyQuery($slug: String!) {
|
|
61
114
|
post(where: {slug: $slug}, stage: PUBLISHED) {
|
|
115
|
+
date
|
|
116
|
+
language
|
|
62
117
|
title
|
|
118
|
+
slug
|
|
119
|
+
author {
|
|
120
|
+
name
|
|
121
|
+
}
|
|
63
122
|
contentInRichText {
|
|
64
|
-
text
|
|
65
123
|
markdown
|
|
124
|
+
html
|
|
66
125
|
}
|
|
67
126
|
excerpt
|
|
68
127
|
tags
|
|
@@ -76,13 +135,16 @@ class GraphQL(DB):
|
|
|
76
135
|
author
|
|
77
136
|
comment
|
|
78
137
|
date: createdAt
|
|
79
|
-
}
|
|
138
|
+
}
|
|
80
139
|
}
|
|
81
140
|
}
|
|
82
|
-
"""
|
|
83
|
-
|
|
141
|
+
"""
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
post_raw = self.client.execute(post, variable_values={"slug": slug})["post"]
|
|
145
|
+
return Post.model_validate(_standarize_post(post_raw))
|
|
84
146
|
|
|
85
|
-
#TODO Cleanup page logic of internationalization (now it depends on translation of slugs)
|
|
147
|
+
# TODO Cleanup page logic of internationalization (now it depends on translation of slugs)
|
|
86
148
|
def get_page(self, slug):
|
|
87
149
|
post = gql(
|
|
88
150
|
"""
|
|
@@ -96,8 +158,9 @@ class GraphQL(DB):
|
|
|
96
158
|
}
|
|
97
159
|
}
|
|
98
160
|
}
|
|
99
|
-
"""
|
|
100
|
-
|
|
161
|
+
"""
|
|
162
|
+
)
|
|
163
|
+
return self.client.execute(post, variable_values={"slug": slug})["page"]
|
|
101
164
|
|
|
102
165
|
def get_posts_by_tag(self, tag, lang):
|
|
103
166
|
post = gql(
|
|
@@ -117,8 +180,11 @@ class GraphQL(DB):
|
|
|
117
180
|
}
|
|
118
181
|
}
|
|
119
182
|
}
|
|
120
|
-
"""
|
|
121
|
-
|
|
183
|
+
"""
|
|
184
|
+
)
|
|
185
|
+
return self.client.execute(post, variable_values={"tag": tag, "lang": lang})[
|
|
186
|
+
"posts"
|
|
187
|
+
]
|
|
122
188
|
|
|
123
189
|
def get_all_providers(self):
|
|
124
190
|
all_providers = gql(
|
|
@@ -151,7 +217,7 @@ class GraphQL(DB):
|
|
|
151
217
|
"""
|
|
152
218
|
)
|
|
153
219
|
query = self.client.execute(all_questions)
|
|
154
|
-
return query[
|
|
220
|
+
return query["questions"]
|
|
155
221
|
|
|
156
222
|
def add_comment(self, author_name, comment, post_slug):
|
|
157
223
|
add_comment = gql(
|
|
@@ -167,7 +233,31 @@ class GraphQL(DB):
|
|
|
167
233
|
id
|
|
168
234
|
}
|
|
169
235
|
}
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
236
|
+
"""
|
|
237
|
+
)
|
|
238
|
+
self.client.execute(
|
|
239
|
+
add_comment,
|
|
240
|
+
variable_values={
|
|
241
|
+
"author": author_name,
|
|
242
|
+
"comment": comment,
|
|
243
|
+
"slug": post_slug,
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def get_font(self):
|
|
248
|
+
return str("")
|
|
249
|
+
|
|
250
|
+
def get_logo_url(self):
|
|
251
|
+
return ""
|
|
252
|
+
|
|
253
|
+
def get_primary_color(self) -> Color:
|
|
254
|
+
return Color()
|
|
255
|
+
|
|
256
|
+
def get_secondary_color(self):
|
|
257
|
+
return Color()
|
|
258
|
+
|
|
259
|
+
def get_site_content(self):
|
|
260
|
+
return ""
|
|
261
|
+
|
|
262
|
+
def get_plugins_data(self):
|
|
263
|
+
return []
|