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.
platzky/db/graph_ql_db.py CHANGED
@@ -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
- def get_db(config):
10
- endpoint = config["DB"]["CMS_ENDPOINT"]
11
- token = config["DB"]["CMS_TOKEN"]
12
- return GraphQL(endpoint, token)
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
- full_token = 'bearer ' + token
18
- transport = AIOHTTPTransport(url=endpoint, headers={'Authorization': full_token})
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
- return self.client.execute(all_posts, variable_values={"lang": lang})['posts']
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)['menuItems']
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
- return self.client.execute(post, variable_values={"slug": slug})['post']
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
- return self.client.execute(post, variable_values={"slug": slug})['page']
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
- return self.client.execute(post, variable_values={"tag": tag, "lang": lang})['posts']
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['questions']
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
- self.client.execute(add_comment, variable_values={
172
- "author": author_name, "comment": comment, "slug": post_slug
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 []
platzky/db/json_db.py CHANGED
@@ -1,34 +1,72 @@
1
- from platzky.blog.db import DB
2
1
  import datetime
2
+ from typing import Any, Dict
3
+
4
+ from pydantic import Field
5
+
6
+ from .db import DB, DBConfig
7
+ from ..models import MenuItem, Post
8
+
9
+
10
+ def db_config_type():
11
+ return JsonDbConfig
12
+
13
+
14
+ class JsonDbConfig(DBConfig):
15
+ data: Dict[str, Any] = Field(alias="DATA")
3
16
 
4
17
 
5
18
  def get_db(config):
6
- return Json(config["DATA"])
19
+ json_db_config = JsonDbConfig.model_validate(config)
20
+ return Json(json_db_config.data)
21
+
22
+
23
+ def db_from_config(config: JsonDbConfig):
24
+ return Json(config.data)
7
25
 
8
26
 
9
27
  class Json(DB):
10
- def __init__(self, data_dict):
11
- self.data = data_dict
28
+ def __init__(self, data: Dict[str, Any]):
29
+ super().__init__()
30
+ self.data: Dict[str, Any] = data
31
+ self.module_name = "json_db"
32
+ self.db_name = "JsonDb"
12
33
 
13
34
  def get_all_posts(self, lang):
14
- posts = (filter(lambda x: x["language"] == lang, self.data.get("posts", [])))
15
- return list(posts)
35
+ return [
36
+ Post.model_validate(post)
37
+ for post in self.get_site_content().get("posts", ())
38
+ if post["language"] == lang
39
+ ]
16
40
 
17
- def get_post(self, slug):
18
- post = next(filter(lambda x: x["slug"] == slug, self.data["posts"]), None)
19
- return post
41
+ def get_post(self, slug: str) -> Post:
42
+ """Returns a post matching the given slug."""
43
+ all_posts = self.get_site_content().get("posts")
44
+ if all_posts is None:
45
+ raise ValueError("Posts data is missing")
46
+ wanted_post = next((post for post in all_posts if post["slug"] == slug), None)
47
+ if wanted_post is None:
48
+ raise ValueError(f"Post with slug {slug} not found")
49
+ return Post.model_validate(wanted_post)
20
50
 
51
+ # TODO: add test for non-existing page
21
52
  def get_page(self, slug):
22
- post = next(filter(lambda x: x["slug"] == slug, self.data["pages"]), None)
23
- return post
53
+ list_of_pages = (
54
+ page
55
+ for page in self.get_site_content().get("pages")
56
+ if page["slug"] == slug
57
+ )
58
+ page = Post.model_validate(next(list_of_pages))
59
+ return page
24
60
 
25
- def get_menu_items(self):
26
- post = self.data.get("menu_items", [])
27
- return post
61
+ def get_menu_items(self) -> list[MenuItem]:
62
+ menu_items_raw = self.get_site_content().get("menu_items", [])
63
+ menu_items_list = [MenuItem.model_validate(x) for x in menu_items_raw]
64
+ return menu_items_list
28
65
 
29
66
  def get_posts_by_tag(self, tag, lang):
30
- posts = filter(lambda x: tag in x["tags"], self.data["posts"])
31
- return posts
67
+ return (
68
+ post for post in self.get_site_content()["posts"] if tag in post["tags"]
69
+ )
32
70
 
33
71
  def get_all_providers(self):
34
72
  return self.data["providers"]
@@ -36,11 +74,37 @@ class Json(DB):
36
74
  def get_all_questions(self):
37
75
  return self.data["questions"]
38
76
 
77
+ def get_site_content(self):
78
+ content = self.data.get("site_content")
79
+ if content is None:
80
+ raise Exception("Content should not be None")
81
+ return content
82
+
83
+ def get_logo_url(self):
84
+ return self.get_site_content().get("logo_url", "")
85
+
86
+ def get_font(self) -> str:
87
+ return self.get_site_content().get("font", "")
88
+
89
+ def get_primary_color(self):
90
+ return self.get_site_content().get("primary_color", "white")
91
+
92
+ def get_secondary_color(self):
93
+ return self.get_site_content().get("secondary_color", "navy")
94
+
39
95
  def add_comment(self, author_name, comment, post_slug):
40
96
  comment = {
41
97
  "author": str(author_name),
42
98
  "comment": str(comment),
43
- "date": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
99
+ "date": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
44
100
  }
45
- post_index = next(i for i in range(len(self.data["posts"])) if self.data["posts"][i]["slug"] == post_slug)
46
- self.data["posts"][post_index]["comments"].append(comment)
101
+
102
+ post_index = next(
103
+ i
104
+ for i in range(len(self.get_site_content()["posts"]))
105
+ if self.get_site_content()["posts"][i]["slug"] == post_slug
106
+ )
107
+ self.get_site_content()["posts"][post_index]["comments"].append(comment)
108
+
109
+ def get_plugins_data(self):
110
+ return self.data.get("plugins", [])
@@ -1,25 +1,41 @@
1
1
  import json
2
- import os.path
3
2
 
4
- from platzky.db.json_db import Json
3
+ from pydantic import Field
4
+
5
+ from .db import DBConfig
6
+ from .json_db import Json
7
+
8
+
9
+ def db_config_type():
10
+ return JsonFileDbConfig
11
+
12
+
13
+ class JsonFileDbConfig(DBConfig):
14
+ path: str = Field(alias="PATH")
5
15
 
6
16
 
7
17
  def get_db(config):
8
- db_path = os.path.abspath(config["DB"]["PATH"])
9
- return JsonFile(db_path)
18
+ json_file_db_config = JsonFileDbConfig.model_validate(config)
19
+ return JsonFile(json_file_db_config.path)
20
+
21
+
22
+ def db_from_config(config: JsonFileDbConfig):
23
+ return JsonFile(config.path)
10
24
 
11
25
 
12
26
  class JsonFile(Json):
13
- def __init__(self, file_path):
14
- self.data_file_path = file_path
27
+ def __init__(self, path: str):
28
+ self.data_file_path = path
15
29
  with open(self.data_file_path) as json_file:
16
30
  data = json.load(json_file)
17
31
  super().__init__(data)
32
+ self.module_name = "json_file_db"
33
+ self.db_name = "JsonFileDb"
18
34
 
19
- def _save_file(self):
20
- with open(self.data_file_path, 'w') as json_file:
35
+ def __save_file(self):
36
+ with open(self.data_file_path, "w") as json_file:
21
37
  json.dump(self.data, json_file)
22
38
 
23
39
  def add_comment(self, author_name, comment, post_slug):
24
40
  super().add_comment(author_name, comment, post_slug)
25
- self._save_file()
41
+ self.__save_file()
platzky/models.py ADDED
@@ -0,0 +1,64 @@
1
+ from pydantic import BaseModel
2
+ import datetime
3
+ import humanize
4
+
5
+
6
+ class Image(BaseModel):
7
+ url: str = ""
8
+ alternateText: str = ""
9
+
10
+
11
+ class MenuItem(BaseModel):
12
+ name: str
13
+ url: str
14
+
15
+
16
+ class Comment(BaseModel):
17
+ author: str
18
+ comment: str
19
+ date: str # TODO change its type to datetime
20
+
21
+ @property
22
+ def time_delta(self) -> str:
23
+ now = datetime.datetime.now()
24
+ date = datetime.datetime.strptime(self.date.split(".")[0], "%Y-%m-%dT%H:%M:%S")
25
+ return humanize.naturaltime(now - date)
26
+
27
+
28
+ class Post(BaseModel):
29
+ author: str
30
+ slug: str
31
+ title: str
32
+ contentInMarkdown: str
33
+ comments: list[Comment]
34
+ excerpt: str
35
+ tags: list[str]
36
+ language: str
37
+ coverImage: Image
38
+ date: str
39
+
40
+ def __lt__(self, other):
41
+ if isinstance(other, Post):
42
+ return self.date < other.date
43
+ return NotImplemented
44
+
45
+
46
+ Page = Post
47
+
48
+
49
+ class Color(BaseModel):
50
+ def __init__(self, r: int = 0, g: int = 0, b: int = 0, a: int = 255):
51
+ if not (0 <= r <= 255):
52
+ raise ValueError("r must be between 0 and 255")
53
+ if not (0 <= g <= 255):
54
+ raise ValueError("g must be between 0 and 255")
55
+ if not (0 <= b <= 255):
56
+ raise ValueError("b must be between 0 and 255")
57
+ if not (0 <= a <= 255):
58
+ raise ValueError("a must be between 0 and 255")
59
+ super().__init__(r=r, g=g, b=b, a=a)
60
+
61
+ r: int
62
+ g: int
63
+ b: int
64
+ a: int
platzky/platzky.py CHANGED
@@ -1,94 +1,154 @@
1
- from flask import Flask, request, session, redirect, render_template
1
+ import typing as t
2
+ import urllib.parse
3
+
4
+ from flask import Flask, redirect, render_template, request, session
2
5
  from flask_babel import Babel
3
6
  from flask_minify import Minify
4
7
 
5
- import os
6
- import urllib.parse
7
-
8
- from . import config, db_loader
9
8
  from .blog import blog
9
+ from .config import (
10
+ Config,
11
+ languages_dict,
12
+ )
13
+ from .db.db_loader import get_db
10
14
  from .plugin_loader import plugify
11
15
  from .seo import seo
12
- from .www_handler import redirect_www_to_nonwww, redirect_nonwww_to_www
13
-
14
-
15
- def create_app_from_config(config_object):
16
- engine = create_engine_from_config(config_object)
17
-
18
- blog_blueprint = blog.create_blog_blueprint(db=engine.db,
19
- config=engine.config, babel=engine.babel)
20
- seo_blueprint = seo.create_seo_blueprint(db=engine.db,
21
- config=engine.config)
22
- engine.register_blueprint(blog_blueprint)
23
- engine.register_blueprint(seo_blueprint)
24
- Minify(app=engine, html=True, js=True, cssless=True)
25
- return engine
26
-
27
-
28
- def create_app(config_path):
29
- absolute_config_path = os.path.join(os.getcwd(), config_path)
30
- config_object = config.from_file(absolute_config_path)
31
- return create_app_from_config(config_object)
32
-
33
-
34
- def create_engine_from_config(config_object):
35
- config_dict = config_object.asdict()
36
- db_driver = db_loader.load_db_driver(config_dict["DB"]["TYPE"])
37
- db = db_driver.get_db(config_dict)
38
- languages = config_dict["LANGUAGES"]
39
- domain_langs = config_dict["DOMAIN_TO_LANG"]
40
- return create_engine(config_dict, db, languages, domain_langs)
16
+ from .www_handler import redirect_nonwww_to_www, redirect_www_to_nonwww
17
+
18
+
19
+ class Engine(Flask):
20
+ def __init__(self, config: Config, db, import_name):
21
+ super().__init__(import_name)
22
+ self.config.from_mapping(config.model_dump(by_alias=True))
23
+ self.db = db
24
+ self.notifiers = []
25
+ self.dynamic_body = ""
26
+ self.dynamic_head = ""
27
+ babel_translation_directories = ";".join(config.translation_directories)
28
+ self.babel = Babel(
29
+ self,
30
+ locale_selector=self.get_locale,
31
+ default_translation_directories=babel_translation_directories,
32
+ )
33
+
34
+ def notify(self, message: str):
35
+ for notifier in self.notifiers:
36
+ notifier(message)
37
+
38
+ def add_notifier(self, notifier):
39
+ self.notifiers.append(notifier)
40
+
41
+ def add_dynamic_body(self, body: str):
42
+ self.dynamic_body += body
43
+
44
+ def add_dynamic_head(self, body: str):
45
+ self.dynamic_head += body
46
+
47
+ def get_locale(self) -> str:
48
+ domain = request.headers["Host"]
49
+ domain_to_lang = self.config.get("DOMAIN_TO_LANG")
50
+
51
+ languages = self.config.get("LANGUAGES", {}).keys()
52
+ backup_lang = session.get(
53
+ "language",
54
+ request.accept_languages.best_match(languages, "en"),
55
+ )
56
+
57
+ if domain_to_lang:
58
+ lang = domain_to_lang.get(domain, backup_lang)
59
+ else:
60
+ lang = backup_lang
41
61
 
62
+ session["language"] = lang
63
+ return lang
42
64
 
43
- def create_engine(config, db, languages, domain_langs):
44
- app = Flask(__name__)
45
- app.config.from_mapping(config)
46
65
 
47
- app.db = db
48
- app.babel = Babel(app)
49
- languages = languages
50
- domain_langs = domain_langs
66
+ def create_engine(config: Config, db) -> Engine:
67
+ app = Engine(config, db, __name__)
51
68
 
52
69
  @app.before_request
53
70
  def handle_www_redirection():
54
- if app.config["USE_WWW"]:
71
+ if config.use_www:
55
72
  return redirect_nonwww_to_www()
56
73
  else:
57
74
  return redirect_www_to_nonwww()
58
75
 
59
- @app.babel.localeselector
60
- def get_locale():
61
- domain = request.headers['Host']
62
- lang = domain_langs.get(domain,
63
- session.get('language',
64
- request.accept_languages.best_match(languages.keys(), 'en')))
65
- session['language'] = lang
66
- return lang
67
-
68
- def get_langs_domain(lang):
69
- return languages.get(lang).get('domain')
76
+ def get_langs_domain(lang: str) -> t.Optional[str]:
77
+ lang_cfg = config.languages.get(lang)
78
+ if lang_cfg is None:
79
+ return None
80
+ return lang_cfg.domain
70
81
 
71
- @app.route('/lang/<string:lang>', methods=["GET"])
82
+ @app.route("/lang/<string:lang>", methods=["GET"])
72
83
  def change_language(lang):
73
84
  if new_domain := get_langs_domain(lang):
74
85
  return redirect("http://" + new_domain, code=301)
75
86
  else:
76
- session['language'] = lang
87
+ session["language"] = lang
77
88
  return redirect(request.referrer)
78
89
 
90
+ @app.route("/logo", methods=["GET"])
91
+ def logo():
92
+ return redirect(
93
+ "https://www.problematy.pl/wp-content/uploads/2023/08/kolor_poziom.png"
94
+ )
95
+
96
+ def url_link(x: str) -> str:
97
+ return urllib.parse.quote(x, safe="")
98
+
79
99
  @app.context_processor
80
100
  def utils():
101
+ locale = app.get_locale()
102
+ flag = lang.flag if (lang := config.languages.get(locale)) is not None else ""
81
103
  return {
82
- "app_name": app.config["APP_NAME"],
83
- 'languages': languages,
84
- "current_flag": languages[get_locale()]['flag'],
85
- "current_language": get_locale(),
86
- "url_link": lambda x: urllib.parse.quote(x, safe=''),
87
- "menu_items": app.db.get_menu_items()
104
+ "app_name": config.app_name,
105
+ "languages": languages_dict(config.languages),
106
+ "current_flag": flag,
107
+ "current_language": locale,
108
+ "url_link": url_link,
109
+ "menu_items": app.db.get_menu_items(),
110
+ "logo_url": app.db.get_logo_url(),
111
+ "font": app.db.get_font(),
112
+ "primary_color": app.db.get_primary_color(),
113
+ "secondary_color": app.db.get_secondary_color(),
88
114
  }
89
115
 
116
+ @app.context_processor
117
+ def dynamic_body():
118
+ return {"dynamic_body": app.dynamic_body}
119
+
120
+ @app.context_processor
121
+ def dynamic_head():
122
+ return {"dynamic_head": app.dynamic_head}
123
+
90
124
  @app.errorhandler(404)
91
125
  def page_not_found(e):
92
- return render_template('404.html', title='404'), 404
126
+ return render_template("404.html", title="404"), 404
93
127
 
94
128
  return plugify(app)
129
+
130
+
131
+ def create_app_from_config(config: Config) -> Engine:
132
+ engine = create_engine_from_config(config)
133
+ blog_blueprint = blog.create_blog_blueprint(
134
+ db=engine.db,
135
+ blog_prefix=config.blog_prefix,
136
+ locale_func=engine.get_locale,
137
+ )
138
+ seo_blueprint = seo.create_seo_blueprint(db=engine.db, config=engine.config)
139
+ engine.register_blueprint(blog_blueprint)
140
+ engine.register_blueprint(seo_blueprint)
141
+
142
+ Minify(app=engine, html=True, js=True, cssless=True)
143
+ return engine
144
+
145
+
146
+ def create_engine_from_config(config: Config) -> Engine:
147
+ """Create an engine from a config."""
148
+ db = get_db(config.db)
149
+ return create_engine(config, db)
150
+
151
+
152
+ def create_app(config_path: str) -> Engine:
153
+ config = Config.parse_yaml(config_path)
154
+ return create_app_from_config(config)