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/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)
platzky/plugin_loader.py CHANGED
@@ -1,42 +1,39 @@
1
+ import importlib.util
1
2
  import os
2
3
  import sys
3
- from os.path import dirname, abspath
4
- import importlib.util
5
- import pkgutil
6
-
7
-
8
- def get_selected_not_installed_plugins(enabled_plugins):
9
- plugins_root_dir = os.path.join(dirname(abspath(__file__)), 'plugins')
10
- plugins_dirs = set(os.listdir(plugins_root_dir))
11
-
12
- return enabled_plugins - plugins_dirs
4
+ from os.path import abspath, dirname
5
+
6
+
7
+ def find_plugin(plugin_name):
8
+ """Find plugin by name and return it as module.
9
+ :param plugin_name: name of plugin to find
10
+ :return: module of plugin
11
+ """
12
+ plugins_dir = os.path.join(dirname(abspath(__file__)), "plugins")
13
+ module_name = plugin_name.removesuffix(".py")
14
+ spec = importlib.util.spec_from_file_location(
15
+ module_name, os.path.join(plugins_dir, plugin_name, "entrypoint.py")
16
+ )
17
+ assert spec is not None
18
+ plugin = importlib.util.module_from_spec(spec)
19
+ sys.modules[module_name] = plugin
20
+ assert spec.loader is not None
21
+ spec.loader.exec_module(plugin)
22
+ return plugin
13
23
 
14
24
 
15
- def find_plugins(enabled_plugins):
16
- plugins_dir = os.path.join(dirname(abspath(__file__)), 'plugins')
17
- plugins = []
18
-
19
- if selected_not_enabled := get_selected_not_installed_plugins(enabled_plugins):
20
- raise Exception(f"Plugins {selected_not_enabled} has been selected in config, but has not been installed.")
21
-
22
- for plugin_dir in enabled_plugins:
23
- module_name = plugin_dir.removesuffix('.py')
24
- spec = importlib.util.spec_from_file_location(module_name,
25
- os.path.join(plugins_dir, plugin_dir, "entrypoint.py"))
26
- plugin = importlib.util.module_from_spec(spec)
27
- sys.modules[module_name] = plugin
28
- spec.loader.exec_module(plugin)
29
- plugins.append(plugin)
30
-
31
- for finder, name, ispkg in pkgutil.iter_modules():
32
- if name.startswith('platzky_'):
33
- plugins.append(importlib.import_module(name))
25
+ def plugify(app):
26
+ """Load plugins and run their entrypoints.
27
+ :param app: Flask app
28
+ :return: Flask app
29
+ """
34
30
 
35
- return plugins
31
+ plugins_data = app.db.get_plugins_data()
36
32
 
33
+ for plugin_data in plugins_data:
34
+ plugin_config = plugin_data["config"]
35
+ plugin_name = plugin_data["name"]
36
+ plugin = find_plugin(plugin_name)
37
+ plugin.process(app, plugin_config)
37
38
 
38
- def plugify(app):
39
- plugins = set(app.config["PLUGINS"])
40
- for plugin in find_plugins(plugins):
41
- plugin.process(app)
42
39
  return app
@@ -1,17 +1,21 @@
1
1
  from flask import redirect
2
- from functools import partial
3
2
  from gql import gql
3
+ from pydantic import BaseModel
4
4
 
5
5
 
6
- def json_get_redirections(self):
6
+ def json_db_get_redirections(self):
7
7
  return self.data.get("redirections", {})
8
8
 
9
9
 
10
- def google_get_redirections(self):
10
+ def json_file_db_get_redirections(self):
11
+ return json_db_get_redirections(self)
12
+
13
+
14
+ def google_json_db_get_redirections(self):
11
15
  return self.data.get("redirections", {})
12
16
 
13
17
 
14
- def graphql_get_redirections(self):
18
+ def graph_ql_db_get_redirections(self):
15
19
  redirections = gql(
16
20
  """
17
21
  query MyQuery{
@@ -22,25 +26,43 @@ def graphql_get_redirections(self):
22
26
  }
23
27
  """
24
28
  )
25
- return {x['source']:x['destination'] for x in self.client.execute(redirections)['redirections']}
29
+ return {
30
+ x["source"]: x["destination"]
31
+ for x in self.client.execute(redirections)["redirections"]
32
+ }
26
33
 
27
34
 
28
- def get_proper_redirections(db_type):
29
- redirections = {
30
- "json_file": json_get_redirections,
31
- "graph_ql": graphql_get_redirections,
32
- "google_json": google_get_redirections
35
+ class Redirection(BaseModel):
36
+ source: str
37
+ destiny: str
33
38
 
34
- }
35
- return redirections[db_type]
36
39
 
40
+ def parse_redirections(config: dict) -> list[Redirection]:
41
+ return [
42
+ Redirection(source=source, destiny=destiny)
43
+ for source, destiny in config.items()
44
+ ]
45
+
46
+
47
+ def setup_routes(app, redirections):
48
+ for redirection in redirections:
49
+ func = redirect_with_name(
50
+ redirection.destiny,
51
+ code=301,
52
+ name=f"{redirection.source}-{redirection.destiny}",
53
+ )
54
+ app.route(rule=redirection.source)(func)
55
+
56
+
57
+ def redirect_with_name(destiny, code, name):
58
+ def named_redirect(*args, **kwargs):
59
+ return redirect(destiny, code, *args, **kwargs)
60
+
61
+ named_redirect.__name__ = name
62
+ return named_redirect
37
63
 
38
- def process(app):
39
- app.db.get_redirections = get_proper_redirections(app.config["DB"]["TYPE"])
40
- redirects = app.db.get_redirections(app.db)
41
- for source, destiny in redirects.items():
42
- func = partial(redirect, destiny, code=301)
43
- func.__name__ = f"{source}-{destiny}"
44
- app.route(rule=source)(func)
45
64
 
65
+ def process(app, config: dict) -> object:
66
+ redirections = parse_redirections(config)
67
+ setup_routes(app, redirections)
46
68
  return app
@@ -1,10 +1,14 @@
1
1
  import smtplib
2
- from functools import partial
3
2
 
3
+ from pydantic import BaseModel, Field
4
4
 
5
- def send_mail(sender_email, password, smtp_server, port, receiver_email, subject, message):
6
- full_message = f'From: {sender_email}\nTo: {receiver_email}\nSubject: {subject}\n\n{message}'
7
5
 
6
+ def send_mail(
7
+ sender_email, password, smtp_server, port, receiver_email, subject, message
8
+ ):
9
+ full_message = (
10
+ f"From: {sender_email}\nTo: {receiver_email}\nSubject: {subject}\n\n{message}"
11
+ )
8
12
  server = smtplib.SMTP_SSL(smtp_server, port)
9
13
  server.ehlo()
10
14
  server.login(sender_email, password)
@@ -12,11 +16,28 @@ def send_mail(sender_email, password, smtp_server, port, receiver_email, subject
12
16
  server.close()
13
17
 
14
18
 
15
- def process(app):
16
- smtp_setup = app.config["SMTP"]
17
- app.sendmail = partial(send_mail,
18
- smtp_setup["ADDRESS"],
19
- smtp_setup["PASSWORD"],
20
- smtp_setup["SERVER"],
21
- smtp_setup["PORT"])
19
+ class SendMailConfig(BaseModel):
20
+ user: str = Field(alias="sender_email")
21
+ password: str = Field(alias="password")
22
+ server: str = Field(alias="smtp_server")
23
+ port: int = Field(alias="port")
24
+ receiver: str = Field(alias="receiver_email")
25
+ subject: str = Field(alias="subject")
26
+
27
+
28
+ def process(app, config):
29
+ plugin_config = SendMailConfig.model_validate(config)
30
+
31
+ def notify(message):
32
+ send_mail(
33
+ sender_email=plugin_config.user,
34
+ password=plugin_config.password,
35
+ smtp_server=plugin_config.server,
36
+ port=plugin_config.port,
37
+ receiver_email=plugin_config.receiver,
38
+ subject=plugin_config.subject,
39
+ message=message,
40
+ )
41
+
42
+ app.add_notifier(notify)
22
43
  return app
platzky/seo/seo.py CHANGED
@@ -1,27 +1,34 @@
1
+ import typing as t
1
2
  import urllib.parse
2
3
  from os.path import dirname
3
- from flask import request, render_template, make_response, Blueprint, current_app
4
+ from flask import Blueprint, current_app, make_response, render_template, request
4
5
 
5
6
 
6
- def create_seo_blueprint(db, config):
7
- url_prefix = config["SEO_PREFIX"]
8
- seo = Blueprint('seo', __name__, url_prefix=url_prefix, template_folder=f'{dirname(__file__)}/../templates')
9
- # secure_headers = SecureHeaders()
7
+ def create_seo_blueprint(db, config: dict[str, t.Any]):
8
+ seo = Blueprint(
9
+ "seo",
10
+ __name__,
11
+ url_prefix=config["SEO_PREFIX"],
12
+ template_folder=f"{dirname(__file__)}/../templates",
13
+ )
10
14
 
11
15
  @seo.route("/robots.txt")
12
16
  def robots():
13
- robots_response = render_template("robots.txt", domain=request.host, mimetype='text/plain')
17
+ robots_response = render_template(
18
+ "robots.txt", domain=request.host, mimetype="text/plain"
19
+ )
14
20
  response = make_response(robots_response)
15
21
  response.headers["Content-Type"] = "text/plain"
16
22
  return response
17
23
 
18
24
  @seo.route("/sitemap.xml")
19
25
  def main_sitemap():
20
- if domain_to_lang := config.get("DOMAIN_TO_LANG", None):
21
- domains_lang = domain_to_lang[request.host]
22
- return sitemap(domains_lang)
26
+ if domain_to_lang := config["DOMAIN_TO_LANG"]:
27
+ return sitemap(domain_to_lang[request.host])
23
28
  else:
24
- return sitemap(config.get("BABEL_TRANSLATION_DIRECTORIES")) #TODO should be based on localization not on config
29
+ return sitemap(
30
+ config.get("TRANSLATION_DIRECTORIES")
31
+ ) # TODO should be based on localization not on config
25
32
 
26
33
  def sitemap(lang):
27
34
  """
@@ -38,28 +45,31 @@ def create_seo_blueprint(db, config):
38
45
  static_urls = list()
39
46
  for rule in current_app.url_map.iter_rules():
40
47
  if not str(rule).startswith("/admin") and not str(rule).startswith("/user"):
41
- if "GET" in rule.methods and len(rule.arguments) == 0:
42
- url = {
43
- "loc": f"{host_base}{str(rule)}"
44
- }
48
+ if (
49
+ rule.methods is not None
50
+ and "GET" in rule.methods
51
+ and len(rule.arguments) == 0
52
+ ):
53
+ url = {"loc": f"{host_base}{str(rule)}"}
45
54
  static_urls.append(url)
46
55
 
47
56
  # Dynamic routes with dynamic content
48
57
  dynamic_urls = list()
49
58
  seo_posts = db.get_all_posts(lang)
50
59
  for post in seo_posts:
51
- slug = post['slug']
52
- datet = post['date'].split('T')[0]
53
- url = {
54
- "loc": f"{host_base}/{slug}",
55
- "lastmod": datet
56
- }
60
+ slug = post["slug"]
61
+ datet = post["date"].split("T")[0]
62
+ url = {"loc": f"{host_base}/{slug}", "lastmod": datet}
57
63
  dynamic_urls.append(url)
58
64
 
59
- statics = list({v['loc']: v for v in static_urls}.values())
60
- dynamics = list({v['loc']: v for v in dynamic_urls}.values())
61
- xml_sitemap = render_template("sitemap.xml", static_urls=statics, dynamic_urls=dynamics,
62
- host_base=host_base)
65
+ statics = list({v["loc"]: v for v in static_urls}.values())
66
+ dynamics = list({v["loc"]: v for v in dynamic_urls}.values())
67
+ xml_sitemap = render_template(
68
+ "sitemap.xml",
69
+ static_urls=statics,
70
+ dynamic_urls=dynamics,
71
+ host_base=host_base,
72
+ )
63
73
  response = make_response(xml_sitemap)
64
74
  response.headers["Content-Type"] = "application/xml"
65
75
  return response
platzky/static/blog.css CHANGED
@@ -81,29 +81,22 @@ img::-moz-selection {
81
81
  background: transparent;
82
82
  }
83
83
 
84
- #mainNav {
85
- # position: absolute;
86
- # border-bottom: 1px solid #e9ecef;
87
- # background-color: white;
88
- font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
89
- }
90
-
91
84
  #mainNav .navbar-brand {
92
- font-weight: 800;
85
+ font-weight: 1000;
93
86
  color: #343a40;
94
87
  }
95
88
 
96
89
  #mainNav .navbar-toggler {
97
- font-size: 12px;
90
+ font-size: 20px;
98
91
  font-weight: 800;
99
- padding: 13px;
92
+ padding: 10px;
100
93
  text-transform: uppercase;
101
94
  color: #343a40;
102
95
  }
103
96
 
104
97
  #mainNav .navbar-nav > li.nav-item > a {
105
- font-size: 12px;
106
- font-weight: 800;
98
+ font-size: 15px;
99
+ font-weight: 1000;
107
100
  letter-spacing: 1px;
108
101
  text-transform: uppercase;
109
102
  }