platzky 0.3.6__py3-none-any.whl → 0.4.3__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/db.py +12 -3
- platzky/db/graph_ql_db.py +20 -5
- platzky/db/json_db.py +8 -0
- platzky/db/mongodb_db.py +143 -0
- platzky/engine.py +71 -2
- {platzky-0.3.6.dist-info → platzky-0.4.3.dist-info}/METADATA +4 -2
- {platzky-0.3.6.dist-info → platzky-0.4.3.dist-info}/RECORD +8 -7
- {platzky-0.3.6.dist-info → platzky-0.4.3.dist-info}/WHEEL +1 -1
platzky/db/db.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any, Callable
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
-
from platzky.models import
|
|
7
|
+
from platzky.models import MenuItem, Page, Post
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class DB(ABC):
|
|
@@ -81,11 +81,11 @@ class DB(ABC):
|
|
|
81
81
|
pass
|
|
82
82
|
|
|
83
83
|
@abstractmethod
|
|
84
|
-
def get_primary_color(self) ->
|
|
84
|
+
def get_primary_color(self) -> str:
|
|
85
85
|
pass
|
|
86
86
|
|
|
87
87
|
@abstractmethod
|
|
88
|
-
def get_secondary_color(self) ->
|
|
88
|
+
def get_secondary_color(self) -> str:
|
|
89
89
|
pass
|
|
90
90
|
|
|
91
91
|
@abstractmethod
|
|
@@ -96,6 +96,15 @@ class DB(ABC):
|
|
|
96
96
|
def get_font(self) -> str:
|
|
97
97
|
pass
|
|
98
98
|
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def health_check(self) -> None:
|
|
101
|
+
"""Perform a health check on the database.
|
|
102
|
+
|
|
103
|
+
Should raise an exception if the database is not healthy.
|
|
104
|
+
This should be a lightweight operation suitable for health checks.
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
99
108
|
|
|
100
109
|
class DBConfig(BaseModel):
|
|
101
110
|
type: str = Field(alias="TYPE")
|
platzky/db/graph_ql_db.py
CHANGED
|
@@ -7,7 +7,7 @@ from gql.transport.exceptions import TransportQueryError
|
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
9
|
from platzky.db.db import DB, DBConfig
|
|
10
|
-
from platzky.models import
|
|
10
|
+
from platzky.models import Post
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def db_config_type():
|
|
@@ -289,11 +289,11 @@ class GraphQL(DB):
|
|
|
289
289
|
|
|
290
290
|
return self.client.execute(favicon)["favicons"][0]["favicon"]["url"]
|
|
291
291
|
|
|
292
|
-
def get_primary_color(self) ->
|
|
293
|
-
return
|
|
292
|
+
def get_primary_color(self) -> str:
|
|
293
|
+
return "white" # Default color as string
|
|
294
294
|
|
|
295
|
-
def get_secondary_color(self):
|
|
296
|
-
return
|
|
295
|
+
def get_secondary_color(self) -> str:
|
|
296
|
+
return "navy" # Default color as string
|
|
297
297
|
|
|
298
298
|
def get_plugins_data(self):
|
|
299
299
|
plugins_data = gql(
|
|
@@ -307,3 +307,18 @@ class GraphQL(DB):
|
|
|
307
307
|
"""
|
|
308
308
|
)
|
|
309
309
|
return self.client.execute(plugins_data)["pluginConfigs"]
|
|
310
|
+
|
|
311
|
+
def health_check(self) -> None:
|
|
312
|
+
"""Perform a health check on the GraphQL database.
|
|
313
|
+
|
|
314
|
+
Raises an exception if the database is not accessible.
|
|
315
|
+
"""
|
|
316
|
+
# Simple query to check connectivity
|
|
317
|
+
health_query = gql(
|
|
318
|
+
"""
|
|
319
|
+
query {
|
|
320
|
+
__typename
|
|
321
|
+
}
|
|
322
|
+
"""
|
|
323
|
+
)
|
|
324
|
+
self.client.execute(health_query)
|
platzky/db/json_db.py
CHANGED
|
@@ -116,3 +116,11 @@ class Json(DB):
|
|
|
116
116
|
|
|
117
117
|
def get_plugins_data(self):
|
|
118
118
|
return self.data.get("plugins", [])
|
|
119
|
+
|
|
120
|
+
def health_check(self) -> None:
|
|
121
|
+
"""Perform a health check on the JSON database.
|
|
122
|
+
|
|
123
|
+
Raises an exception if the database is not accessible.
|
|
124
|
+
"""
|
|
125
|
+
# Try to access site_content to ensure basic structure is valid
|
|
126
|
+
self._get_site_content()
|
platzky/db/mongodb_db.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pymongo import MongoClient
|
|
6
|
+
from pymongo.collection import Collection
|
|
7
|
+
from pymongo.database import Database
|
|
8
|
+
|
|
9
|
+
from platzky.db.db import DB, DBConfig
|
|
10
|
+
from platzky.models import MenuItem, Page, Post
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def db_config_type():
|
|
14
|
+
return MongoDbConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MongoDbConfig(DBConfig):
|
|
18
|
+
connection_string: str = Field(alias="CONNECTION_STRING")
|
|
19
|
+
database_name: str = Field(alias="DATABASE_NAME")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_db(config):
|
|
23
|
+
mongodb_config = MongoDbConfig.model_validate(config)
|
|
24
|
+
return MongoDB(mongodb_config.connection_string, mongodb_config.database_name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def db_from_config(config: MongoDbConfig):
|
|
28
|
+
return MongoDB(config.connection_string, config.database_name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MongoDB(DB):
|
|
32
|
+
def __init__(self, connection_string: str, database_name: str):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.connection_string = connection_string
|
|
35
|
+
self.database_name = database_name
|
|
36
|
+
self.client: MongoClient[Any] = MongoClient(connection_string)
|
|
37
|
+
self.db: Database[Any] = self.client[database_name]
|
|
38
|
+
self.module_name = "mongodb_db"
|
|
39
|
+
self.db_name = "MongoDB"
|
|
40
|
+
|
|
41
|
+
# Collection references
|
|
42
|
+
self.site_content: Collection[Any] = self.db.site_content
|
|
43
|
+
self.posts: Collection[Any] = self.db.posts
|
|
44
|
+
self.pages: Collection[Any] = self.db.pages
|
|
45
|
+
self.menu_items: Collection[Any] = self.db.menu_items
|
|
46
|
+
self.plugins: Collection[Any] = self.db.plugins
|
|
47
|
+
|
|
48
|
+
def get_app_description(self, lang: str) -> str:
|
|
49
|
+
site_content = self.site_content.find_one({"_id": "config"})
|
|
50
|
+
if site_content and "app_description" in site_content:
|
|
51
|
+
return site_content["app_description"].get(lang, "")
|
|
52
|
+
return ""
|
|
53
|
+
|
|
54
|
+
def get_all_posts(self, lang: str) -> list[Post]:
|
|
55
|
+
posts_cursor = self.posts.find({"language": lang})
|
|
56
|
+
return [Post.model_validate(post) for post in posts_cursor]
|
|
57
|
+
|
|
58
|
+
def get_menu_items_in_lang(self, lang: str) -> list[MenuItem]:
|
|
59
|
+
menu_items_doc = self.menu_items.find_one({"_id": lang})
|
|
60
|
+
if menu_items_doc and "items" in menu_items_doc:
|
|
61
|
+
return [MenuItem.model_validate(item) for item in menu_items_doc["items"]]
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
def get_post(self, slug: str) -> Post:
|
|
65
|
+
post_doc = self.posts.find_one({"slug": slug})
|
|
66
|
+
if post_doc is None:
|
|
67
|
+
raise ValueError(f"Post with slug {slug} not found")
|
|
68
|
+
return Post.model_validate(post_doc)
|
|
69
|
+
|
|
70
|
+
def get_page(self, slug: str) -> Page:
|
|
71
|
+
page_doc = self.pages.find_one({"slug": slug})
|
|
72
|
+
if page_doc is None:
|
|
73
|
+
raise ValueError(f"Page with slug {slug} not found")
|
|
74
|
+
return Page.model_validate(page_doc)
|
|
75
|
+
|
|
76
|
+
def get_posts_by_tag(self, tag: str, lang: str) -> Any:
|
|
77
|
+
posts_cursor = self.posts.find({"tags": tag, "language": lang})
|
|
78
|
+
return posts_cursor
|
|
79
|
+
|
|
80
|
+
def add_comment(self, author_name: str, comment: str, post_slug: str) -> None:
|
|
81
|
+
now_utc = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
|
|
82
|
+
comment_doc = {
|
|
83
|
+
"author": str(author_name),
|
|
84
|
+
"comment": str(comment),
|
|
85
|
+
"date": now_utc,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
result = self.posts.update_one({"slug": post_slug}, {"$push": {"comments": comment_doc}})
|
|
89
|
+
if result.matched_count == 0:
|
|
90
|
+
raise ValueError(f"Post with slug {post_slug} not found")
|
|
91
|
+
|
|
92
|
+
def get_logo_url(self) -> str:
|
|
93
|
+
site_content = self.site_content.find_one({"_id": "config"})
|
|
94
|
+
if site_content:
|
|
95
|
+
return site_content.get("logo_url", "")
|
|
96
|
+
return ""
|
|
97
|
+
|
|
98
|
+
def get_favicon_url(self) -> str:
|
|
99
|
+
site_content = self.site_content.find_one({"_id": "config"})
|
|
100
|
+
if site_content:
|
|
101
|
+
return site_content.get("favicon_url", "")
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
def get_primary_color(self) -> str:
|
|
105
|
+
site_content = self.site_content.find_one({"_id": "config"})
|
|
106
|
+
if site_content:
|
|
107
|
+
return site_content.get("primary_color", "white")
|
|
108
|
+
return "white"
|
|
109
|
+
|
|
110
|
+
def get_secondary_color(self) -> str:
|
|
111
|
+
site_content = self.site_content.find_one({"_id": "config"})
|
|
112
|
+
if site_content:
|
|
113
|
+
return site_content.get("secondary_color", "navy")
|
|
114
|
+
return "navy"
|
|
115
|
+
|
|
116
|
+
def get_plugins_data(self) -> list[Any]:
|
|
117
|
+
plugins_doc = self.plugins.find_one({"_id": "config"})
|
|
118
|
+
if plugins_doc and "data" in plugins_doc:
|
|
119
|
+
return plugins_doc["data"]
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
def get_font(self) -> str:
|
|
123
|
+
site_content = self.site_content.find_one({"_id": "config"})
|
|
124
|
+
if site_content:
|
|
125
|
+
return site_content.get("font", "")
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
def health_check(self) -> None:
|
|
129
|
+
"""Perform a health check on the MongoDB database.
|
|
130
|
+
|
|
131
|
+
Raises an exception if the database is not accessible.
|
|
132
|
+
"""
|
|
133
|
+
# Simple ping to check if database is accessible
|
|
134
|
+
self.client.admin.command("ping")
|
|
135
|
+
|
|
136
|
+
def _close_connection(self) -> None:
|
|
137
|
+
"""Close the MongoDB connection"""
|
|
138
|
+
if self.client:
|
|
139
|
+
self.client.close()
|
|
140
|
+
|
|
141
|
+
def __del__(self):
|
|
142
|
+
"""Ensure connection is closed when object is destroyed"""
|
|
143
|
+
self._close_connection()
|
platzky/engine.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
|
3
|
+
from typing import Any, Callable, Dict, List, Tuple
|
|
3
4
|
|
|
4
|
-
from flask import Flask, request, session
|
|
5
|
+
from flask import Blueprint, Flask, jsonify, make_response, request, session
|
|
5
6
|
from flask_babel import Babel
|
|
6
7
|
|
|
7
8
|
from platzky.config import Config
|
|
@@ -17,6 +18,7 @@ class Engine(Flask):
|
|
|
17
18
|
self.login_methods = []
|
|
18
19
|
self.dynamic_body = ""
|
|
19
20
|
self.dynamic_head = ""
|
|
21
|
+
self.health_checks: List[Tuple[str, Callable[[], None]]] = []
|
|
20
22
|
directory = os.path.dirname(os.path.realpath(__file__))
|
|
21
23
|
locale_dir = os.path.join(directory, "locale")
|
|
22
24
|
config.translation_directories.append(locale_dir)
|
|
@@ -26,6 +28,7 @@ class Engine(Flask):
|
|
|
26
28
|
locale_selector=self.get_locale,
|
|
27
29
|
default_translation_directories=babel_translation_directories,
|
|
28
30
|
)
|
|
31
|
+
self._register_default_health_endpoints()
|
|
29
32
|
|
|
30
33
|
self.cms_modules: List[CmsModule] = []
|
|
31
34
|
# TODO add plugins as CMS Module - all plugins should be visible from
|
|
@@ -69,3 +72,69 @@ class Engine(Flask):
|
|
|
69
72
|
|
|
70
73
|
session["language"] = lang
|
|
71
74
|
return lang
|
|
75
|
+
|
|
76
|
+
def add_health_check(self, name: str, check_function: Callable[[], None]) -> None:
|
|
77
|
+
"""Register a health check function"""
|
|
78
|
+
if not callable(check_function):
|
|
79
|
+
raise TypeError(f"check_function must be callable, got {type(check_function)}")
|
|
80
|
+
self.health_checks.append((name, check_function))
|
|
81
|
+
|
|
82
|
+
def _register_default_health_endpoints(self):
|
|
83
|
+
"""Register default health endpoints"""
|
|
84
|
+
|
|
85
|
+
health_bp = Blueprint("health", __name__)
|
|
86
|
+
HEALTH_CHECK_TIMEOUT = 10 # seconds
|
|
87
|
+
|
|
88
|
+
@health_bp.route("/health/liveness")
|
|
89
|
+
def liveness():
|
|
90
|
+
"""Simple liveness check - is the app running?"""
|
|
91
|
+
return jsonify({"status": "alive"}), 200
|
|
92
|
+
|
|
93
|
+
@health_bp.route("/health/readiness")
|
|
94
|
+
def readiness():
|
|
95
|
+
"""Readiness check - can the app serve traffic?"""
|
|
96
|
+
health_status: Dict[str, Any] = {"status": "ready", "checks": {}}
|
|
97
|
+
status_code = 200
|
|
98
|
+
|
|
99
|
+
executor = ThreadPoolExecutor(max_workers=1)
|
|
100
|
+
try:
|
|
101
|
+
# Database health check with timeout
|
|
102
|
+
future = executor.submit(self.db.health_check)
|
|
103
|
+
try:
|
|
104
|
+
future.result(timeout=HEALTH_CHECK_TIMEOUT)
|
|
105
|
+
health_status["checks"]["database"] = "ok"
|
|
106
|
+
except TimeoutError:
|
|
107
|
+
health_status["checks"]["database"] = "failed: timeout"
|
|
108
|
+
health_status["status"] = "not_ready"
|
|
109
|
+
status_code = 503
|
|
110
|
+
except Exception as e:
|
|
111
|
+
health_status["checks"]["database"] = f"failed: {e!s}"
|
|
112
|
+
health_status["status"] = "not_ready"
|
|
113
|
+
status_code = 503
|
|
114
|
+
|
|
115
|
+
# Run application-registered health checks
|
|
116
|
+
for check_name, check_func in self.health_checks:
|
|
117
|
+
future = executor.submit(check_func)
|
|
118
|
+
try:
|
|
119
|
+
future.result(timeout=HEALTH_CHECK_TIMEOUT)
|
|
120
|
+
health_status["checks"][check_name] = "ok"
|
|
121
|
+
except TimeoutError:
|
|
122
|
+
health_status["checks"][check_name] = "failed: timeout"
|
|
123
|
+
health_status["status"] = "not_ready"
|
|
124
|
+
status_code = 503
|
|
125
|
+
except Exception as e:
|
|
126
|
+
health_status["checks"][check_name] = f"failed: {e!s}"
|
|
127
|
+
health_status["status"] = "not_ready"
|
|
128
|
+
status_code = 503
|
|
129
|
+
finally:
|
|
130
|
+
# Shutdown without waiting if any futures are still running
|
|
131
|
+
executor.shutdown(wait=False)
|
|
132
|
+
|
|
133
|
+
return make_response(jsonify(health_status), status_code)
|
|
134
|
+
|
|
135
|
+
# Simple /health alias for liveness
|
|
136
|
+
@health_bp.route("/health")
|
|
137
|
+
def health():
|
|
138
|
+
return liveness()
|
|
139
|
+
|
|
140
|
+
self.register_blueprint(health_bp)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: platzky
|
|
3
|
-
Version: 0.3
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Not only blog engine
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.10,<4.0
|
|
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
14
|
Requires-Dist: Flask (==3.0.3)
|
|
14
15
|
Requires-Dist: Flask-Babel (>=4.0.0,<5.0.0)
|
|
15
16
|
Requires-Dist: Flask-Minify (>=0.42,<0.43)
|
|
@@ -22,6 +23,7 @@ Requires-Dist: gql (>=3.4.0,<4.0.0)
|
|
|
22
23
|
Requires-Dist: humanize (>=4.9.0,<5.0.0)
|
|
23
24
|
Requires-Dist: pydantic (>=2.7.1,<3.0.0)
|
|
24
25
|
Requires-Dist: pygithub (>=2.6.1,<3.0.0)
|
|
26
|
+
Requires-Dist: pymongo (>=4.7.0,<5.0.0)
|
|
25
27
|
Description-Content-Type: text/markdown
|
|
26
28
|
|
|
27
29
|

|
|
@@ -10,14 +10,15 @@ platzky/blog/comment_form.py,sha256=4lkNJ_S_2DZmJBbz-NPDqahvy2Zz5AGNH2spFeGIop4,
|
|
|
10
10
|
platzky/config.py,sha256=M3gmZI9yI-ThgmTA4RKsAPcnJwJjcWhXipYzq3hO-Hk,2346
|
|
11
11
|
platzky/db/README.md,sha256=IO-LoDsd4dLBZenaz423EZjvEOQu_8m2OC0G7du170w,1753
|
|
12
12
|
platzky/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
platzky/db/db.py,sha256=
|
|
13
|
+
platzky/db/db.py,sha256=0h5rGCBO_N1wBqJRl5EoiW_bFDpNIvmNwuA0hJi89jw,3060
|
|
14
14
|
platzky/db/db_loader.py,sha256=CuEiXxhIa4bFMm0vi7ugzm7j3WycilGRKCU6smgIImE,905
|
|
15
15
|
platzky/db/github_json_db.py,sha256=G1GBIomeKOCeG05pA4qccaFntiGzkgyEMQJz_FQlvNY,2185
|
|
16
16
|
platzky/db/google_json_db.py,sha256=rS__UEK7ed71htTg066_vzpg0etTlpke6YkcrAQ3Fgk,1325
|
|
17
|
-
platzky/db/graph_ql_db.py,sha256=
|
|
18
|
-
platzky/db/json_db.py,sha256
|
|
17
|
+
platzky/db/graph_ql_db.py,sha256=af6yy1R27YO8N9zJWU7VgU7optRgpdk_1ZUtab_1eT4,8967
|
|
18
|
+
platzky/db/json_db.py,sha256=NUBPy4jt-y37TYq4SCGaSgief3MbBWL_Efw8Bxp8Jo0,4046
|
|
19
19
|
platzky/db/json_file_db.py,sha256=tPo92n5zG7vGpunn5vl66zISHBziQdxBttitvc5hPug,1030
|
|
20
|
-
platzky/
|
|
20
|
+
platzky/db/mongodb_db.py,sha256=28KO8XmTEiqE7FcNBzw_pfxOy6Vo-T7qsHdUlh59QX0,5174
|
|
21
|
+
platzky/engine.py,sha256=Kv242PsB8lVz_FCYdGogd8o5zGmn5Msev3B3lfYRUXA,5411
|
|
21
22
|
platzky/locale/en/LC_MESSAGES/messages.po,sha256=WaZGlFAegKRq7CSz69dWKic-mKvQFhVvssvExxNmGaU,1400
|
|
22
23
|
platzky/locale/pl/LC_MESSAGES/messages.po,sha256=sUPxMKDeEOoZ5UIg94rGxZD06YVWiAMWIby2XE51Hrc,1624
|
|
23
24
|
platzky/models.py,sha256=DZZgKW2Q3fY2GMdikFUmAgpsRqT5VKAOwP6RmEsmO2M,1871
|
|
@@ -39,6 +40,6 @@ platzky/templates/post.html,sha256=GSgjIZsOQKtNx3cEbquSjZ5L4whPnG6MzRyoq9k4B8Q,1
|
|
|
39
40
|
platzky/templates/robots.txt,sha256=2_j2tiYtYJnzZUrANiX9pvBxyw5Dp27fR_co18BPEJ0,116
|
|
40
41
|
platzky/templates/sitemap.xml,sha256=iIJZ91_B5ZuNLCHsRtsGKZlBAXojOTP8kffqKLacgvs,578
|
|
41
42
|
platzky/www_handler.py,sha256=pF6Rmvem1sdVqHD7z3RLrDuG-CwAqfGCti50_NPsB2w,725
|
|
42
|
-
platzky-0.3.
|
|
43
|
-
platzky-0.3.
|
|
44
|
-
platzky-0.3.
|
|
43
|
+
platzky-0.4.3.dist-info/METADATA,sha256=RMfCt6cM7vEEWaS_lmahJZ9kNyy53c41_gmu064X6tU,1818
|
|
44
|
+
platzky-0.4.3.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
45
|
+
platzky-0.4.3.dist-info/RECORD,,
|