platzky 1.1.0__py3-none-any.whl → 1.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/db/graph_ql_db.py CHANGED
@@ -1,5 +1,8 @@
1
- # TODO rename file, extract it to another library, remove qgl and aiohttp from dependencies
1
+ """GraphQL-based database implementation for CMS integration."""
2
2
 
3
+ # TODO: Rename file, extract to another library, remove gql and aiohttp from dependencies
4
+
5
+ from typing import Any
3
6
 
4
7
  from gql import Client, gql
5
8
  from gql.transport.aiohttp import AIOHTTPTransport
@@ -7,29 +10,60 @@ from gql.transport.exceptions import TransportQueryError
7
10
  from pydantic import Field
8
11
 
9
12
  from platzky.db.db import DB, DBConfig
10
- from platzky.models import Post
13
+ from platzky.models import MenuItem, Page, Post
14
+
11
15
 
16
+ def db_config_type() -> type["GraphQlDbConfig"]:
17
+ """Return the configuration class for GraphQL database.
12
18
 
13
- def db_config_type():
19
+ Returns:
20
+ GraphQlDbConfig class
21
+ """
14
22
  return GraphQlDbConfig
15
23
 
16
24
 
17
25
  class GraphQlDbConfig(DBConfig):
26
+ """Configuration for GraphQL database connection."""
27
+
18
28
  endpoint: str = Field(alias="CMS_ENDPOINT")
19
29
  token: str = Field(alias="CMS_TOKEN")
20
30
 
21
31
 
22
- def get_db(config: GraphQlDbConfig):
32
+ def get_db(config: GraphQlDbConfig) -> "GraphQL":
33
+ """Get a GraphQL database instance from configuration.
34
+
35
+ Args:
36
+ config: GraphQL database configuration
37
+
38
+ Returns:
39
+ Configured GraphQL database instance
40
+ """
23
41
  return GraphQL(config.endpoint, config.token)
24
42
 
25
43
 
26
- def db_from_config(config: GraphQlDbConfig):
44
+ def db_from_config(config: GraphQlDbConfig) -> "GraphQL":
45
+ """Create a GraphQL database instance from configuration.
46
+
47
+ Args:
48
+ config: GraphQL database configuration
49
+
50
+ Returns:
51
+ Configured GraphQL database instance
52
+ """
27
53
  return GraphQL(config.endpoint, config.token)
28
54
 
29
55
 
30
- def _standarize_comment(
31
- comment,
32
- ):
56
+ def _standardize_comment(
57
+ comment: dict[str, Any],
58
+ ) -> dict[str, Any]:
59
+ """Standardize comment data structure from GraphQL response.
60
+
61
+ Args:
62
+ comment: Raw comment data from GraphQL response
63
+
64
+ Returns:
65
+ Standardized comment dictionary
66
+ """
33
67
  return {
34
68
  "author": comment["author"],
35
69
  "comment": comment["comment"],
@@ -37,14 +71,22 @@ def _standarize_comment(
37
71
  }
38
72
 
39
73
 
40
- def _standarize_post(post):
74
+ def _standardize_post(post: dict[str, Any]) -> dict[str, Any]:
75
+ """Standardize post data structure from GraphQL response.
76
+
77
+ Args:
78
+ post: Raw post data from GraphQL response
79
+
80
+ Returns:
81
+ Standardized post dictionary
82
+ """
41
83
  return {
42
84
  "author": post["author"]["name"],
43
85
  "slug": post["slug"],
44
86
  "title": post["title"],
45
87
  "excerpt": post["excerpt"],
46
88
  "contentInMarkdown": post["contentInRichText"]["html"],
47
- "comments": [_standarize_comment(comment) for comment in post["comments"]],
89
+ "comments": [_standardize_comment(comment) for comment in post["comments"]],
48
90
  "tags": post["tags"],
49
91
  "language": post["language"],
50
92
  "coverImage": {
@@ -54,8 +96,72 @@ def _standarize_post(post):
54
96
  }
55
97
 
56
98
 
99
+ def _standardize_page(page: dict[str, Any]) -> dict[str, Any]:
100
+ """Standardize page data structure from GraphQL response.
101
+
102
+ Pages have fewer required fields than posts in the GraphQL schema.
103
+ This function provides sensible defaults for missing Post fields.
104
+
105
+ Args:
106
+ page: Raw page data from GraphQL response
107
+
108
+ Returns:
109
+ Standardized page dictionary compatible with Page model
110
+ """
111
+ return {
112
+ "author": page.get("author", ""),
113
+ "slug": page.get("slug", ""),
114
+ "title": page["title"],
115
+ "excerpt": page.get("excerpt", ""),
116
+ "contentInMarkdown": page["contentInMarkdown"],
117
+ "comments": [],
118
+ "tags": page.get("tags", []),
119
+ "language": page.get("language", "en"),
120
+ "coverImage": {
121
+ "url": page.get("coverImage", {}).get("url", ""),
122
+ },
123
+ "date": page.get("date", "1970-01-01"),
124
+ }
125
+
126
+
127
+ def _standardize_post_by_tag(post: dict[str, Any]) -> dict[str, Any]:
128
+ """Standardize post data from get_posts_by_tag GraphQL response.
129
+
130
+ Posts returned by tag query have fewer fields than full posts.
131
+ This function provides sensible defaults for missing Post fields.
132
+
133
+ Args:
134
+ post: Raw post data from GraphQL get_posts_by_tag response
135
+
136
+ Returns:
137
+ Standardized post dictionary compatible with Post model
138
+ """
139
+ return {
140
+ "author": post.get("author", ""),
141
+ "slug": post["slug"],
142
+ "title": post["title"],
143
+ "excerpt": post["excerpt"],
144
+ "contentInMarkdown": post.get("contentInMarkdown", ""),
145
+ "comments": [],
146
+ "tags": post["tags"],
147
+ "language": post.get("language", "en"),
148
+ "coverImage": {
149
+ "url": post["coverImage"]["image"]["url"],
150
+ },
151
+ "date": post["date"],
152
+ }
153
+
154
+
57
155
  class GraphQL(DB):
58
- def __init__(self, endpoint, token):
156
+ """GraphQL database implementation for CMS integration."""
157
+
158
+ def __init__(self, endpoint: str, token: str) -> None:
159
+ """Initialize GraphQL database connection.
160
+
161
+ Args:
162
+ endpoint: GraphQL API endpoint URL
163
+ token: Authentication token for the API
164
+ """
59
165
  self.module_name = "graph_ql_db"
60
166
  self.db_name = "GraphQLDb"
61
167
  full_token = "bearer " + token
@@ -63,7 +169,15 @@ class GraphQL(DB):
63
169
  self.client = Client(transport=transport)
64
170
  super().__init__()
65
171
 
66
- def get_all_posts(self, lang):
172
+ def get_all_posts(self, lang: str) -> list[Post]:
173
+ """Retrieve all published posts for a specific language.
174
+
175
+ Args:
176
+ lang: Language code (e.g., 'en', 'pl')
177
+
178
+ Returns:
179
+ List of Post objects
180
+ """
67
181
  all_posts = gql(
68
182
  """
69
183
  query MyQuery($lang: Lang!) {
@@ -98,9 +212,17 @@ class GraphQL(DB):
98
212
  )
99
213
  raw_ql_posts = self.client.execute(all_posts, variable_values={"lang": lang})["posts"]
100
214
 
101
- return [Post.model_validate(_standarize_post(post)) for post in raw_ql_posts]
215
+ return [Post.model_validate(_standardize_post(post)) for post in raw_ql_posts]
216
+
217
+ def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
218
+ """Retrieve menu items for a specific language.
219
+
220
+ Args:
221
+ lang: Language code (e.g., 'en', 'pl')
102
222
 
103
- def get_menu_items_in_lang(self, lang):
223
+ Returns:
224
+ List of MenuItem objects
225
+ """
104
226
  menu_items = []
105
227
  try:
106
228
  menu_items_with_lang = gql(
@@ -132,9 +254,17 @@ class GraphQL(DB):
132
254
  )
133
255
  menu_items = self.client.execute(menu_items_without_lang)
134
256
 
135
- return menu_items["menuItems"]
257
+ return [MenuItem.model_validate(item) for item in menu_items["menuItems"]]
258
+
259
+ def get_post(self, slug: str) -> Post:
260
+ """Retrieve a single post by its slug.
136
261
 
137
- def get_post(self, slug):
262
+ Args:
263
+ slug: URL-friendly identifier for the post
264
+
265
+ Returns:
266
+ Post object
267
+ """
138
268
  post = gql(
139
269
  """
140
270
  query MyQuery($slug: String!) {
@@ -169,14 +299,23 @@ class GraphQL(DB):
169
299
  )
170
300
 
171
301
  post_raw = self.client.execute(post, variable_values={"slug": slug})["post"]
172
- return Post.model_validate(_standarize_post(post_raw))
302
+ return Post.model_validate(_standardize_post(post_raw))
173
303
 
174
- # TODO Cleanup page logic of internationalization (now it depends on translation of slugs)
175
- def get_page(self, slug):
176
- post = gql(
304
+ # TODO: Cleanup page logic of internationalization (now it depends on translation of slugs)
305
+ def get_page(self, slug: str) -> Page:
306
+ """Retrieve a page by its slug.
307
+
308
+ Args:
309
+ slug: URL-friendly identifier for the page
310
+
311
+ Returns:
312
+ Page object
313
+ """
314
+ page_query = gql(
177
315
  """
178
316
  query MyQuery ($slug: String!){
179
317
  page(where: {slug: $slug}, stage: PUBLISHED) {
318
+ slug
180
319
  title
181
320
  contentInMarkdown
182
321
  coverImage
@@ -187,9 +326,19 @@ class GraphQL(DB):
187
326
  }
188
327
  """
189
328
  )
190
- return self.client.execute(post, variable_values={"slug": slug})["page"]
329
+ page_raw = self.client.execute(page_query, variable_values={"slug": slug})["page"]
330
+ return Page.model_validate(_standardize_page(page_raw))
331
+
332
+ def get_posts_by_tag(self, tag: str, lang: str) -> list[Post]:
333
+ """Retrieve posts filtered by tag and language.
334
+
335
+ Args:
336
+ tag: Tag name to filter by
337
+ lang: Language code (e.g., 'en', 'pl')
191
338
 
192
- def get_posts_by_tag(self, tag, lang):
339
+ Returns:
340
+ List of Post objects
341
+ """
193
342
  post = gql(
194
343
  """
195
344
  query MyQuery ($tag: String!, $lang: Lang!){
@@ -209,9 +358,17 @@ class GraphQL(DB):
209
358
  }
210
359
  """
211
360
  )
212
- return self.client.execute(post, variable_values={"tag": tag, "lang": lang})["posts"]
361
+ raw_posts = self.client.execute(post, variable_values={"tag": tag, "lang": lang})["posts"]
362
+ return [Post.model_validate(_standardize_post_by_tag(p)) for p in raw_posts]
363
+
364
+ def add_comment(self, author_name: str, comment: str, post_slug: str) -> None:
365
+ """Add a new comment to a post.
213
366
 
214
- def add_comment(self, author_name, comment, post_slug):
367
+ Args:
368
+ author_name: Name of the comment author
369
+ comment: Comment text content
370
+ post_slug: URL-friendly identifier of the post
371
+ """
215
372
  add_comment = gql(
216
373
  """
217
374
  mutation MyMutation($author: String!, $comment: String!, $slug: String!) {
@@ -236,10 +393,20 @@ class GraphQL(DB):
236
393
  },
237
394
  )
238
395
 
239
- def get_font(self):
240
- return str("")
396
+ def get_font(self) -> str:
397
+ """Get the font configuration for the application.
398
+
399
+ Returns:
400
+ Empty string (not implemented in GraphQL backend)
401
+ """
402
+ return ""
403
+
404
+ def get_logo_url(self) -> str:
405
+ """Retrieve the URL of the application logo.
241
406
 
242
- def get_logo_url(self):
407
+ Returns:
408
+ Logo image URL or empty string if not found
409
+ """
243
410
  logo = gql(
244
411
  """
245
412
  query myquery {
@@ -259,7 +426,15 @@ class GraphQL(DB):
259
426
  except IndexError:
260
427
  return ""
261
428
 
262
- def get_app_description(self, lang):
429
+ def get_app_description(self, lang: str) -> str:
430
+ """Retrieve the application description for a specific language.
431
+
432
+ Args:
433
+ lang: Language code (e.g., 'en', 'pl')
434
+
435
+ Returns:
436
+ Application description text or empty string if not found
437
+ """
263
438
  description_query = gql(
264
439
  """
265
440
  query myquery($lang: Lang!) {
@@ -272,9 +447,14 @@ class GraphQL(DB):
272
447
 
273
448
  return self.client.execute(description_query, variable_values={"lang": lang})[
274
449
  "applicationSetups"
275
- ][0].get("applicationDescription", None)
450
+ ][0].get("applicationDescription", "")
276
451
 
277
- def get_favicon_url(self):
452
+ def get_favicon_url(self) -> str:
453
+ """Retrieve the URL of the application favicon.
454
+
455
+ Returns:
456
+ Favicon URL
457
+ """
278
458
  favicon = gql(
279
459
  """
280
460
  query myquery {
@@ -295,7 +475,12 @@ class GraphQL(DB):
295
475
  def get_secondary_color(self) -> str:
296
476
  return "navy" # Default color as string
297
477
 
298
- def get_plugins_data(self):
478
+ def get_plugins_data(self) -> list[dict[str, Any]]:
479
+ """Retrieve configuration data for all plugins.
480
+
481
+ Returns:
482
+ List of plugin configuration dictionaries
483
+ """
299
484
  plugins_data = gql(
300
485
  """
301
486
  query MyQuery {
platzky/db/json_db.py CHANGED
@@ -1,46 +1,92 @@
1
+ """In-memory JSON database implementation."""
2
+
1
3
  import datetime
2
4
  from typing import Any
3
5
 
4
6
  from pydantic import Field
5
7
 
6
8
  from platzky.db.db import DB, DBConfig
7
- from platzky.models import MenuItem, Post
9
+ from platzky.models import MenuItem, Page, Post
10
+
8
11
 
12
+ def db_config_type() -> type["JsonDbConfig"]:
13
+ """Return the configuration class for JSON database.
9
14
 
10
- def db_config_type():
15
+ Returns:
16
+ JsonDbConfig class
17
+ """
11
18
  return JsonDbConfig
12
19
 
13
20
 
14
21
  class JsonDbConfig(DBConfig):
22
+ """Configuration for in-memory JSON database."""
23
+
15
24
  data: dict[str, Any] = Field(alias="DATA")
16
25
 
17
26
 
18
- def get_db(config):
27
+ def get_db(config: dict[str, Any]) -> "Json":
28
+ """Get a JSON database instance from raw configuration.
29
+
30
+ Args:
31
+ config: Raw configuration dictionary
32
+
33
+ Returns:
34
+ Configured JSON database instance
35
+ """
19
36
  json_db_config = JsonDbConfig.model_validate(config)
20
37
  return Json(json_db_config.data)
21
38
 
22
39
 
23
- def db_from_config(config: JsonDbConfig):
24
- return Json(config.data)
40
+ def db_from_config(config: JsonDbConfig) -> "Json":
41
+ """Create a JSON database instance from configuration.
25
42
 
43
+ Args:
44
+ config: JSON database configuration
26
45
 
27
- # TODO make all language specific methods to be available without language
28
- # this will allow to have a default language and if there is one language
29
- # there will be no need to pass it to the method or in db
46
+ Returns:
47
+ Configured JSON database instance
48
+ """
49
+ return Json(config.data)
30
50
 
31
51
 
52
+ # TODO: Make all language-specific methods available without language parameter.
53
+ # This will allow a default language and if there is one language,
54
+ # there will be no need to pass it to the method or in db.
32
55
  class Json(DB):
56
+ """In-memory JSON database implementation."""
57
+
33
58
  def __init__(self, data: dict[str, Any]):
59
+ """Initialize JSON database with data dictionary.
60
+
61
+ Args:
62
+ data: Dictionary containing all database content
63
+ """
34
64
  super().__init__()
35
65
  self.data: dict[str, Any] = data
36
66
  self.module_name = "json_db"
37
67
  self.db_name = "JsonDb"
38
68
 
39
- def get_app_description(self, lang):
69
+ def get_app_description(self, lang: str) -> str:
70
+ """Retrieve the application description for a specific language.
71
+
72
+ Args:
73
+ lang: Language code (e.g., 'en', 'pl')
74
+
75
+ Returns:
76
+ Application description text or empty string if not found
77
+ """
40
78
  description = self._get_site_content().get("app_description", {})
41
- return description.get(lang, None)
79
+ return description.get(lang, "")
42
80
 
43
- def get_all_posts(self, lang):
81
+ def get_all_posts(self, lang: str) -> list[Post]:
82
+ """Retrieve all posts for a specific language.
83
+
84
+ Args:
85
+ lang: Language code (e.g., 'en', 'pl')
86
+
87
+ Returns:
88
+ List of Post objects
89
+ """
44
90
  return [
45
91
  Post.model_validate(post)
46
92
  for post in self._get_site_content().get("posts", ())
@@ -48,7 +94,17 @@ class Json(DB):
48
94
  ]
49
95
 
50
96
  def get_post(self, slug: str) -> Post:
51
- """Returns a post matching the given slug."""
97
+ """Returns a post matching the given slug.
98
+
99
+ Args:
100
+ slug: URL-friendly identifier for the post
101
+
102
+ Returns:
103
+ Post object
104
+
105
+ Raises:
106
+ ValueError: If posts data is missing or post not found
107
+ """
52
108
  all_posts = self._get_site_content().get("posts")
53
109
  if all_posts is None:
54
110
  raise ValueError("Posts data is missing")
@@ -57,59 +113,124 @@ class Json(DB):
57
113
  raise ValueError(f"Post with slug {slug} not found")
58
114
  return Post.model_validate(wanted_post)
59
115
 
60
- # TODO: add test for non-existing page
61
- def get_page(self, slug):
62
- list_of_pages = (
63
- page for page in self._get_site_content().get("pages") if page["slug"] == slug
64
- )
116
+ # TODO: Add test for non-existing page
117
+ def get_page(self, slug: str) -> Page:
118
+ """Retrieve a page by its slug.
119
+
120
+ Args:
121
+ slug: URL-friendly identifier for the page
122
+
123
+ Returns:
124
+ Page object
125
+
126
+ Raises:
127
+ ValueError: If pages data is missing or page not found
128
+ """
129
+ pages = self._get_site_content().get("pages")
130
+ if pages is None:
131
+ raise ValueError("Pages data is missing")
132
+ list_of_pages = (page for page in pages if page["slug"] == slug)
65
133
  wanted_page = next(list_of_pages, None)
66
134
  if wanted_page is None:
67
135
  raise ValueError(f"Page with slug {slug} not found")
68
- page = Post.model_validate(wanted_page)
136
+ page = Page.model_validate(wanted_page)
69
137
  return page
70
138
 
71
- def get_menu_items_in_lang(self, lang) -> list[MenuItem]:
139
+ def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
140
+ """Retrieve menu items for a specific language.
141
+
142
+ Args:
143
+ lang: Language code (e.g., 'en', 'pl')
144
+
145
+ Returns:
146
+ List of MenuItem objects
147
+ """
72
148
  menu_items_raw = self._get_site_content().get("menu_items", {})
73
149
  items_in_lang = menu_items_raw.get(lang, {})
74
150
 
75
151
  menu_items_list = [MenuItem.model_validate(x) for x in items_in_lang]
76
152
  return menu_items_list
77
153
 
78
- def get_posts_by_tag(self, tag, lang):
79
- return (
80
- post
154
+ def get_posts_by_tag(self, tag: str, lang: str) -> list[Post]:
155
+ """Retrieve posts filtered by tag and language.
156
+
157
+ Returns a list of posts, unlike generators which can only be iterated once.
158
+ """
159
+ return [
160
+ Post.model_validate(post)
81
161
  for post in self._get_site_content()["posts"]
82
162
  if tag in post["tags"] and post["language"] == lang
83
- )
163
+ ]
164
+
165
+ def _get_site_content(self) -> dict[str, Any]:
166
+ """Get the site content dictionary from data.
167
+
168
+ Returns:
169
+ Site content dictionary
84
170
 
85
- def _get_site_content(self):
171
+ Raises:
172
+ ValueError: If site content is not found
173
+ """
86
174
  content = self.data.get("site_content")
87
175
  if content is None:
88
- raise Exception("Content should not be None")
176
+ raise ValueError("Content should not be None")
89
177
  return content
90
178
 
91
- def get_logo_url(self):
179
+ def get_logo_url(self) -> str:
180
+ """Retrieve the URL of the application logo.
181
+
182
+ Returns:
183
+ Logo image URL or empty string if not found
184
+ """
92
185
  return self._get_site_content().get("logo_url", "")
93
186
 
94
- def get_favicon_url(self):
187
+ def get_favicon_url(self) -> str:
188
+ """Retrieve the URL of the application favicon.
189
+
190
+ Returns:
191
+ Favicon URL or empty string if not found
192
+ """
95
193
  return self._get_site_content().get("favicon_url", "")
96
194
 
97
195
  def get_font(self) -> str:
196
+ """Get the font configuration for the application.
197
+
198
+ Returns:
199
+ Font name or empty string if not configured
200
+ """
98
201
  return self._get_site_content().get("font", "")
99
202
 
100
- def get_primary_color(self):
203
+ def get_primary_color(self) -> str:
204
+ """Retrieve the primary color for the application theme.
205
+
206
+ Returns:
207
+ Primary color value, defaults to 'white'
208
+ """
101
209
  return self._get_site_content().get("primary_color", "white")
102
210
 
103
- def get_secondary_color(self):
211
+ def get_secondary_color(self) -> str:
212
+ """Retrieve the secondary color for the application theme.
213
+
214
+ Returns:
215
+ Secondary color value, defaults to 'navy'
216
+ """
104
217
  return self._get_site_content().get("secondary_color", "navy")
105
218
 
106
- def add_comment(self, author_name, comment, post_slug):
107
- # Store dates in UTC with timezone info for consistency with MongoDB backend
108
- # This ensures accurate time delta calculations regardless of server timezone
109
- # Legacy dates without timezone info are still supported for backward compatibility
219
+ def add_comment(self, author_name: str, comment: str, post_slug: str) -> None:
220
+ """Add a new comment to a post.
221
+
222
+ Store dates in UTC with timezone info for consistency with MongoDB backend.
223
+ This ensures accurate time delta calculations regardless of server timezone.
224
+ Legacy dates without timezone info are still supported for backward compatibility.
225
+
226
+ Args:
227
+ author_name: Name of the comment author
228
+ comment: Comment text content
229
+ post_slug: URL-friendly identifier of the post
230
+ """
110
231
  now_utc = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
111
232
 
112
- comment = {
233
+ comment_data = {
113
234
  "author": str(author_name),
114
235
  "comment": str(comment),
115
236
  "date": now_utc,
@@ -120,9 +241,14 @@ class Json(DB):
120
241
  for i in range(len(self._get_site_content()["posts"]))
121
242
  if self._get_site_content()["posts"][i]["slug"] == post_slug
122
243
  )
123
- self._get_site_content()["posts"][post_index]["comments"].append(comment)
244
+ self._get_site_content()["posts"][post_index]["comments"].append(comment_data)
245
+
246
+ def get_plugins_data(self) -> list[dict[str, Any]]:
247
+ """Retrieve configuration data for all plugins.
124
248
 
125
- def get_plugins_data(self):
249
+ Returns:
250
+ List of plugin configuration dictionaries
251
+ """
126
252
  return self.data.get("plugins", [])
127
253
 
128
254
  def health_check(self) -> None: