platzky 0.1.19__py3-none-any.whl → 0.2.1__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/blog/__init__.py +0 -0
- platzky/blog/blog.py +61 -35
- platzky/blog/comment_form.py +8 -4
- platzky/config.py +52 -49
- platzky/db/__init__.py +0 -0
- platzky/db/db.py +99 -0
- platzky/db/db_loader.py +32 -0
- platzky/db/google_json_db.py +33 -10
- platzky/db/graph_ql_db.py +113 -57
- platzky/db/json_db.py +81 -23
- platzky/db/json_file_db.py +25 -9
- platzky/models.py +64 -0
- platzky/platzky.py +122 -62
- platzky/plugin_loader.py +31 -34
- platzky/plugins/redirections/entrypoint.py +41 -19
- platzky/plugins/sendmail/entrypoint.py +31 -10
- platzky/seo/seo.py +34 -24
- platzky/static/blog.css +5 -12
- platzky/templates/base.html +137 -22
- platzky/templates/blog.html +3 -2
- platzky/templates/body_meta.html +4 -21
- platzky/templates/feed.xml +1 -1
- platzky/templates/head_meta.html +5 -15
- platzky/templates/post.html +2 -2
- platzky/www_handler.py +7 -4
- platzky-0.2.1.dist-info/METADATA +40 -0
- platzky-0.2.1.dist-info/RECORD +34 -0
- {platzky-0.1.19.dist-info → platzky-0.2.1.dist-info}/WHEEL +1 -1
- platzky/blog/db.py +0 -18
- platzky/blog/post_formatter.py +0 -16
- platzky/db_loader.py +0 -11
- platzky-0.1.19.dist-info/METADATA +0 -43
- platzky-0.1.19.dist-info/RECORD +0 -32
platzky/db/graph_ql_db.py
CHANGED
|
@@ -1,22 +1,58 @@
|
|
|
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
|
-
|
|
3
|
+
|
|
4
|
+
from gql import Client, gql
|
|
4
5
|
from gql.transport.aiohttp import AIOHTTPTransport
|
|
5
|
-
import
|
|
6
|
-
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
|
|
8
|
+
from .db import DB, DBConfig
|
|
9
|
+
from ..models import Color, Post
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def db_config_type():
|
|
13
|
+
return GraphQlDbConfig
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GraphQlDbConfig(DBConfig):
|
|
17
|
+
endpoint: str = Field(alias="CMS_ENDPOINT")
|
|
18
|
+
token: str = Field(alias="CMS_TOKEN")
|
|
19
|
+
|
|
7
20
|
|
|
21
|
+
def get_db(config: GraphQlDbConfig):
|
|
22
|
+
return GraphQL(config.endpoint, config.token)
|
|
8
23
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
24
|
+
|
|
25
|
+
def db_from_config(config: GraphQlDbConfig):
|
|
26
|
+
return GraphQL(config.endpoint, config.token)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _standarize_post(post):
|
|
30
|
+
return {
|
|
31
|
+
"author": post["author"]["name"],
|
|
32
|
+
"slug": post["slug"],
|
|
33
|
+
"title": post["title"],
|
|
34
|
+
"excerpt": post["excerpt"],
|
|
35
|
+
"contentInMarkdown": post["contentInRichText"]["html"],
|
|
36
|
+
"comments": post["comments"],
|
|
37
|
+
"tags": post["tags"],
|
|
38
|
+
"language": post["language"],
|
|
39
|
+
"coverImage": {
|
|
40
|
+
"url": post["coverImage"]["image"]["url"],
|
|
41
|
+
},
|
|
42
|
+
"date": post["date"],
|
|
43
|
+
}
|
|
13
44
|
|
|
14
45
|
|
|
15
46
|
class GraphQL(DB):
|
|
16
47
|
def __init__(self, endpoint, token):
|
|
17
|
-
|
|
18
|
-
|
|
48
|
+
self.module_name = "graph_ql_db"
|
|
49
|
+
self.db_name = "GraphQLDb"
|
|
50
|
+
full_token = "bearer " + token
|
|
51
|
+
transport = AIOHTTPTransport(
|
|
52
|
+
url=endpoint, headers={"Authorization": full_token}
|
|
53
|
+
)
|
|
19
54
|
self.client = Client(transport=transport)
|
|
55
|
+
super().__init__()
|
|
20
56
|
|
|
21
57
|
def get_all_posts(self, lang):
|
|
22
58
|
all_posts = gql(
|
|
@@ -24,22 +60,38 @@ class GraphQL(DB):
|
|
|
24
60
|
query MyQuery($lang: Lang!) {
|
|
25
61
|
posts(where: {language: $lang}, orderBy: date_DESC, stage: PUBLISHED){
|
|
26
62
|
createdAt
|
|
63
|
+
author {
|
|
64
|
+
name
|
|
65
|
+
}
|
|
66
|
+
contentInRichText {
|
|
67
|
+
html
|
|
68
|
+
}
|
|
69
|
+
comments {
|
|
70
|
+
comment
|
|
71
|
+
author
|
|
72
|
+
createdAt
|
|
73
|
+
}
|
|
27
74
|
date
|
|
28
75
|
title
|
|
29
76
|
excerpt
|
|
30
77
|
slug
|
|
31
78
|
tags
|
|
79
|
+
language
|
|
32
80
|
coverImage {
|
|
33
81
|
alternateText
|
|
34
82
|
image {
|
|
35
83
|
url
|
|
36
84
|
}
|
|
37
|
-
}
|
|
85
|
+
}
|
|
38
86
|
}
|
|
39
87
|
}
|
|
40
88
|
"""
|
|
41
89
|
)
|
|
42
|
-
|
|
90
|
+
raw_ql_posts = self.client.execute(all_posts, variable_values={"lang": lang})[
|
|
91
|
+
"posts"
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
return [Post.model_validate(_standarize_post(post)) for post in raw_ql_posts]
|
|
43
95
|
|
|
44
96
|
def get_menu_items(self):
|
|
45
97
|
menu_items = gql(
|
|
@@ -52,17 +104,23 @@ class GraphQL(DB):
|
|
|
52
104
|
}
|
|
53
105
|
"""
|
|
54
106
|
)
|
|
55
|
-
return self.client.execute(menu_items)[
|
|
107
|
+
return self.client.execute(menu_items)["menuItems"]
|
|
56
108
|
|
|
57
109
|
def get_post(self, slug):
|
|
58
110
|
post = gql(
|
|
59
111
|
"""
|
|
60
112
|
query MyQuery($slug: String!) {
|
|
61
113
|
post(where: {slug: $slug}, stage: PUBLISHED) {
|
|
114
|
+
date
|
|
115
|
+
language
|
|
62
116
|
title
|
|
117
|
+
slug
|
|
118
|
+
author {
|
|
119
|
+
name
|
|
120
|
+
}
|
|
63
121
|
contentInRichText {
|
|
64
|
-
text
|
|
65
122
|
markdown
|
|
123
|
+
html
|
|
66
124
|
}
|
|
67
125
|
excerpt
|
|
68
126
|
tags
|
|
@@ -76,13 +134,16 @@ class GraphQL(DB):
|
|
|
76
134
|
author
|
|
77
135
|
comment
|
|
78
136
|
date: createdAt
|
|
79
|
-
}
|
|
137
|
+
}
|
|
80
138
|
}
|
|
81
139
|
}
|
|
82
|
-
"""
|
|
83
|
-
|
|
140
|
+
"""
|
|
141
|
+
)
|
|
84
142
|
|
|
85
|
-
|
|
143
|
+
post_raw = self.client.execute(post, variable_values={"slug": slug})["post"]
|
|
144
|
+
return Post.model_validate(_standarize_post(post_raw))
|
|
145
|
+
|
|
146
|
+
# TODO Cleanup page logic of internationalization (now it depends on translation of slugs)
|
|
86
147
|
def get_page(self, slug):
|
|
87
148
|
post = gql(
|
|
88
149
|
"""
|
|
@@ -96,8 +157,9 @@ class GraphQL(DB):
|
|
|
96
157
|
}
|
|
97
158
|
}
|
|
98
159
|
}
|
|
99
|
-
"""
|
|
100
|
-
|
|
160
|
+
"""
|
|
161
|
+
)
|
|
162
|
+
return self.client.execute(post, variable_values={"slug": slug})["page"]
|
|
101
163
|
|
|
102
164
|
def get_posts_by_tag(self, tag, lang):
|
|
103
165
|
post = gql(
|
|
@@ -117,41 +179,11 @@ class GraphQL(DB):
|
|
|
117
179
|
}
|
|
118
180
|
}
|
|
119
181
|
}
|
|
120
|
-
""")
|
|
121
|
-
return self.client.execute(post, variable_values={"tag": tag, "lang": lang})['posts']
|
|
122
|
-
|
|
123
|
-
def get_all_providers(self):
|
|
124
|
-
all_providers = gql(
|
|
125
|
-
"""
|
|
126
|
-
query MyQuery {
|
|
127
|
-
providers(stage: PUBLISHED) {
|
|
128
|
-
link
|
|
129
|
-
name
|
|
130
|
-
offer
|
|
131
|
-
currency
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
182
|
"""
|
|
135
183
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return providers
|
|
140
|
-
|
|
141
|
-
def get_all_questions(self):
|
|
142
|
-
all_questions = gql(
|
|
143
|
-
"""
|
|
144
|
-
query MyQuery {
|
|
145
|
-
questions(stage: PUBLISHED) {
|
|
146
|
-
question
|
|
147
|
-
field
|
|
148
|
-
inputType
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
"""
|
|
152
|
-
)
|
|
153
|
-
query = self.client.execute(all_questions)
|
|
154
|
-
return query['questions']
|
|
184
|
+
return self.client.execute(post, variable_values={"tag": tag, "lang": lang})[
|
|
185
|
+
"posts"
|
|
186
|
+
]
|
|
155
187
|
|
|
156
188
|
def add_comment(self, author_name, comment, post_slug):
|
|
157
189
|
add_comment = gql(
|
|
@@ -167,7 +199,31 @@ class GraphQL(DB):
|
|
|
167
199
|
id
|
|
168
200
|
}
|
|
169
201
|
}
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
202
|
+
"""
|
|
203
|
+
)
|
|
204
|
+
self.client.execute(
|
|
205
|
+
add_comment,
|
|
206
|
+
variable_values={
|
|
207
|
+
"author": author_name,
|
|
208
|
+
"comment": comment,
|
|
209
|
+
"slug": post_slug,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def get_font(self):
|
|
214
|
+
return str("")
|
|
215
|
+
|
|
216
|
+
def get_logo_url(self):
|
|
217
|
+
return ""
|
|
218
|
+
|
|
219
|
+
def get_primary_color(self) -> Color:
|
|
220
|
+
return Color()
|
|
221
|
+
|
|
222
|
+
def get_secondary_color(self):
|
|
223
|
+
return Color()
|
|
224
|
+
|
|
225
|
+
def get_site_content(self):
|
|
226
|
+
return ""
|
|
227
|
+
|
|
228
|
+
def get_plugins_data(self):
|
|
229
|
+
return []
|
platzky/db/json_db.py
CHANGED
|
@@ -1,46 +1,104 @@
|
|
|
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
|
-
|
|
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,
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
67
|
+
return (
|
|
68
|
+
post for post in self.get_site_content()["posts"] if tag in post["tags"]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def get_site_content(self):
|
|
72
|
+
content = self.data.get("site_content")
|
|
73
|
+
if content is None:
|
|
74
|
+
raise Exception("Content should not be None")
|
|
75
|
+
return content
|
|
76
|
+
|
|
77
|
+
def get_logo_url(self):
|
|
78
|
+
return self.get_site_content().get("logo_url", "")
|
|
32
79
|
|
|
33
|
-
def
|
|
34
|
-
return self.
|
|
80
|
+
def get_font(self) -> str:
|
|
81
|
+
return self.get_site_content().get("font", "")
|
|
35
82
|
|
|
36
|
-
def
|
|
37
|
-
return self.
|
|
83
|
+
def get_primary_color(self):
|
|
84
|
+
return self.get_site_content().get("primary_color", "white")
|
|
85
|
+
|
|
86
|
+
def get_secondary_color(self):
|
|
87
|
+
return self.get_site_content().get("secondary_color", "navy")
|
|
38
88
|
|
|
39
89
|
def add_comment(self, author_name, comment, post_slug):
|
|
40
90
|
comment = {
|
|
41
91
|
"author": str(author_name),
|
|
42
92
|
"comment": str(comment),
|
|
43
|
-
"date": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
|
93
|
+
"date": datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
|
|
44
94
|
}
|
|
45
|
-
|
|
46
|
-
|
|
95
|
+
|
|
96
|
+
post_index = next(
|
|
97
|
+
i
|
|
98
|
+
for i in range(len(self.get_site_content()["posts"]))
|
|
99
|
+
if self.get_site_content()["posts"][i]["slug"] == post_slug
|
|
100
|
+
)
|
|
101
|
+
self.get_site_content()["posts"][post_index]["comments"].append(comment)
|
|
102
|
+
|
|
103
|
+
def get_plugins_data(self):
|
|
104
|
+
return self.data.get("plugins", [])
|
platzky/db/json_file_db.py
CHANGED
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import os.path
|
|
3
2
|
|
|
4
|
-
from
|
|
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
|
-
|
|
9
|
-
return JsonFile(
|
|
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,
|
|
14
|
-
self.data_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
|
|
20
|
-
with open(self.data_file_path,
|
|
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.
|
|
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
|