lombik 0.1.0__tar.gz
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.
- lombik-0.1.0/LICENSE +22 -0
- lombik-0.1.0/PKG-INFO +53 -0
- lombik-0.1.0/README.md +35 -0
- lombik-0.1.0/lombik/__init__.py +0 -0
- lombik-0.1.0/lombik/cli.py +64 -0
- lombik-0.1.0/lombik/templates/__init__.py +0 -0
- lombik-0.1.0/lombik/templates/createapp/app.py +240 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/auth/models.py +71 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/auth/roles.py +4 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/auth/routes.py +41 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/auth/services.py +162 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/core/error_logging.py +27 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/core/models.py +21 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/core/routes.py +19 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/settings/modesl.py +0 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/settings/routes.py +19 -0
- lombik-0.1.0/lombik/templates/createapp/blueprints/settings/services.py +0 -0
- lombik-0.1.0/lombik/templates/createapp/config.py +53 -0
- lombik-0.1.0/lombik/templates/createapp/db.py +5 -0
- lombik-0.1.0/lombik/templates/createapp/models/__init__.py +11 -0
- lombik-0.1.0/lombik/templates/createapp/tools.py +45 -0
- lombik-0.1.0/lombik/templates/createapp/wrappers.py +13 -0
- lombik-0.1.0/lombik.egg-info/PKG-INFO +53 -0
- lombik-0.1.0/lombik.egg-info/SOURCES.txt +28 -0
- lombik-0.1.0/lombik.egg-info/dependency_links.txt +1 -0
- lombik-0.1.0/lombik.egg-info/entry_points.txt +2 -0
- lombik-0.1.0/lombik.egg-info/requires.txt +9 -0
- lombik-0.1.0/lombik.egg-info/top_level.txt +1 -0
- lombik-0.1.0/pyproject.toml +27 -0
- lombik-0.1.0/setup.cfg +4 -0
lombik-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Plajner
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
lombik-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lombik
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flask scaffold engine
|
|
5
|
+
Author: David Plajner
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: Flask
|
|
9
|
+
Requires-Dist: Flask-SQLAlchemy
|
|
10
|
+
Requires-Dist: Flask-Migrate
|
|
11
|
+
Requires-Dist: Flask-WTF
|
|
12
|
+
Requires-Dist: Flask-Session
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: click
|
|
15
|
+
Requires-Dist: SQLAlchemy
|
|
16
|
+
Requires-Dist: Werkzeug
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Lombik
|
|
20
|
+
#### A scaffold engine for Flask, the greatest framework of all times.
|
|
21
|
+
|
|
22
|
+
This readme will serve perfectly as a self documentation file as I go, and at the end of it I will re-write it with the final details.
|
|
23
|
+
That means that if you see this, either my company cleersoftware has taken off and you might work with me on enhancing the scaffold for our apps.
|
|
24
|
+
Another possibility is that lombik took off, and you are reading the early commits. Have fun, there is probably a lot of them.
|
|
25
|
+
Anyways, I'm gonan actually start on explaning / plannign how thigns will work.
|
|
26
|
+
|
|
27
|
+
> Install lombik
|
|
28
|
+
Run `pip install lombik`
|
|
29
|
+
|
|
30
|
+
> Create app
|
|
31
|
+
To create an app called *myapp*, run `lombik createapp myapp` then `cd myapp`
|
|
32
|
+
This creates the entire structure of your application in one go and you can run `flask run --debug`
|
|
33
|
+
|
|
34
|
+
> Create superuser
|
|
35
|
+
Lombik comes with MySQL as a default db. In the generated .env file you'll find the credentials for local development.
|
|
36
|
+
Replace those placeholders with your actual dev credentials and production if you have it already.
|
|
37
|
+
When done, initialize the database by running `flask initdb` and then you can run `flask superuser` to create your owner account.
|
|
38
|
+
With this, you'll be able to log in to your app and start developing from there.
|
|
39
|
+
|
|
40
|
+
This is a quite simple auth system, in fact it is simple by design and meant to be replaced for production apps with something more robust.
|
|
41
|
+
|
|
42
|
+
> Features and benefits
|
|
43
|
+
On top of the structural guidance, lombik comes with a few built in goodies.
|
|
44
|
+
|
|
45
|
+
- User authentication and session handling
|
|
46
|
+
- CSRF / session expiry
|
|
47
|
+
- Error handling
|
|
48
|
+
- Structure
|
|
49
|
+
- Template filters for frontend development
|
|
50
|
+
- Pre-imported libraries
|
|
51
|
+
- General templates for desktop and mobile
|
|
52
|
+
|
|
53
|
+
|
lombik-0.1.0/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Lombik
|
|
2
|
+
#### A scaffold engine for Flask, the greatest framework of all times.
|
|
3
|
+
|
|
4
|
+
This readme will serve perfectly as a self documentation file as I go, and at the end of it I will re-write it with the final details.
|
|
5
|
+
That means that if you see this, either my company cleersoftware has taken off and you might work with me on enhancing the scaffold for our apps.
|
|
6
|
+
Another possibility is that lombik took off, and you are reading the early commits. Have fun, there is probably a lot of them.
|
|
7
|
+
Anyways, I'm gonan actually start on explaning / plannign how thigns will work.
|
|
8
|
+
|
|
9
|
+
> Install lombik
|
|
10
|
+
Run `pip install lombik`
|
|
11
|
+
|
|
12
|
+
> Create app
|
|
13
|
+
To create an app called *myapp*, run `lombik createapp myapp` then `cd myapp`
|
|
14
|
+
This creates the entire structure of your application in one go and you can run `flask run --debug`
|
|
15
|
+
|
|
16
|
+
> Create superuser
|
|
17
|
+
Lombik comes with MySQL as a default db. In the generated .env file you'll find the credentials for local development.
|
|
18
|
+
Replace those placeholders with your actual dev credentials and production if you have it already.
|
|
19
|
+
When done, initialize the database by running `flask initdb` and then you can run `flask superuser` to create your owner account.
|
|
20
|
+
With this, you'll be able to log in to your app and start developing from there.
|
|
21
|
+
|
|
22
|
+
This is a quite simple auth system, in fact it is simple by design and meant to be replaced for production apps with something more robust.
|
|
23
|
+
|
|
24
|
+
> Features and benefits
|
|
25
|
+
On top of the structural guidance, lombik comes with a few built in goodies.
|
|
26
|
+
|
|
27
|
+
- User authentication and session handling
|
|
28
|
+
- CSRF / session expiry
|
|
29
|
+
- Error handling
|
|
30
|
+
- Structure
|
|
31
|
+
- Template filters for frontend development
|
|
32
|
+
- Pre-imported libraries
|
|
33
|
+
- General templates for desktop and mobile
|
|
34
|
+
|
|
35
|
+
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from getpass import getpass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import secrets
|
|
4
|
+
import shutil
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
BASE_DIR = Path(__file__).parent
|
|
9
|
+
|
|
10
|
+
STARTUP_TEMPLATE = BASE_DIR / "templates" / "createapp"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
def cli():
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def is_text_file(path: Path) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
path.read_text(encoding="utf-8")
|
|
20
|
+
return True
|
|
21
|
+
except UnicodeDecodeError:
|
|
22
|
+
return False
|
|
23
|
+
except Exception:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def replace_placeholders(target_dir, replacements):
|
|
28
|
+
|
|
29
|
+
for file in Path(target_dir).rglob("*"):
|
|
30
|
+
|
|
31
|
+
if not file.is_file():
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
content = file.read_text(encoding="utf-8")
|
|
36
|
+
except UnicodeDecodeError:
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
for key, value in replacements.items():
|
|
40
|
+
content = content.replace(key, value)
|
|
41
|
+
|
|
42
|
+
file.write_text(content, encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cli.command()
|
|
46
|
+
@click.argument("name")
|
|
47
|
+
def createapp(name):
|
|
48
|
+
|
|
49
|
+
target = Path.cwd() / name
|
|
50
|
+
|
|
51
|
+
replacements = {
|
|
52
|
+
"{{SECRET_KEY}}": secrets.token_urlsafe(64),
|
|
53
|
+
"{{CRKEY}}": secrets.token_urlsafe(64)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
shutil.copytree(STARTUP_TEMPLATE, target)
|
|
57
|
+
|
|
58
|
+
replace_placeholders(target, replacements)
|
|
59
|
+
|
|
60
|
+
print(f"Created app: {name}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
cli()
|
|
File without changes
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
from flask import Flask, g, session, render_template, send_from_directory, current_app
|
|
2
|
+
from flask.cli import with_appcontext
|
|
3
|
+
from flask_wtf.csrf import CSRFError
|
|
4
|
+
from flask_wtf import CSRFProtect
|
|
5
|
+
from flask_session import Session
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
import time
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
from db import db, migrate
|
|
12
|
+
|
|
13
|
+
from config import config_dict
|
|
14
|
+
from models import load_models
|
|
15
|
+
|
|
16
|
+
# import all your bleurpints here
|
|
17
|
+
from blueprints.core.routes import core_bp
|
|
18
|
+
from blueprints.auth.routes import auth_bp
|
|
19
|
+
from blueprints.settings.routes import settings_bp
|
|
20
|
+
|
|
21
|
+
load_dotenv()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def create_app(env="default"):
|
|
25
|
+
app = Flask(__name__, subdomain_matching=False)
|
|
26
|
+
|
|
27
|
+
load_models()
|
|
28
|
+
# load models first otherwise fetch will never happen
|
|
29
|
+
# also, stop trying to make fetch happen
|
|
30
|
+
_user_management(app)
|
|
31
|
+
|
|
32
|
+
_init_config(app, env)
|
|
33
|
+
_init_extensions(app)
|
|
34
|
+
_init_session_dir(app)
|
|
35
|
+
_init_filters(app)
|
|
36
|
+
_init_blueprints(app)
|
|
37
|
+
_init_hooks(app)
|
|
38
|
+
_init_routes(app)
|
|
39
|
+
_init_error_handlers(app)
|
|
40
|
+
|
|
41
|
+
return app
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _init_config(app, env):
|
|
45
|
+
cfg = config_dict[env]()
|
|
46
|
+
|
|
47
|
+
app.config.from_object(cfg)
|
|
48
|
+
app.config.update(
|
|
49
|
+
SECRET_KEY=cfg.SECRET_KEY,
|
|
50
|
+
CACHE_TYPE=cfg.CACHE_TYPE,
|
|
51
|
+
CACHE_DEFAULT_TIMEOUT=int(cfg.CACHE_DEFAULT_TIMEOUT),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def _init_extensions(app):
|
|
55
|
+
Session(app)
|
|
56
|
+
CSRFProtect(app)
|
|
57
|
+
db.init_app(app)
|
|
58
|
+
migrate.init_app(app, db)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _init_session_dir(app):
|
|
62
|
+
if app.config.get("SESSION_TYPE") == "filesystem":
|
|
63
|
+
os.makedirs(app.config["SESSION_FILE_DIR"], exist_ok=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _init_blueprints(app):
|
|
67
|
+
app.register_blueprint(core_bp, url_prefix="/")
|
|
68
|
+
app.register_blueprint(auth_bp, url_prefix="/auth")
|
|
69
|
+
app.register_blueprint(settings_bp, url_prefix="/settings")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _init_hooks(app):
|
|
73
|
+
#app.before_request(load_user)
|
|
74
|
+
app.before_request(_csrf_lifetime_tracker)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _csrf_lifetime_tracker():
|
|
78
|
+
if "csrf_last_reset" not in session:
|
|
79
|
+
session["csrf_last_reset"] = int(time.time())
|
|
80
|
+
|
|
81
|
+
session["csrf_last_reset"] = int(time.time())
|
|
82
|
+
|
|
83
|
+
g.csrf_time_left = current_app.config.get("WTF_CSRF_TIME_LIMIT", 3600)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
These are all the custom templating commands you can use with lombik.
|
|
88
|
+
|
|
89
|
+
As an example, timestamps in the db are stored as UTC.
|
|
90
|
+
By default, lombik will create the user in the g object with the user's timezone
|
|
91
|
+
|
|
92
|
+
Now to display the UTC timestamp to the user's local timestamp all you have to do is:
|
|
93
|
+
|
|
94
|
+
{{ created_at | localtime }}
|
|
95
|
+
|
|
96
|
+
And it convert UTC to user's local.
|
|
97
|
+
|
|
98
|
+
You can also do dateonly, which by default converts it to UTC and strips out the date.
|
|
99
|
+
There is more.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
def _init_filters(app):
|
|
103
|
+
from markdown import markdown
|
|
104
|
+
|
|
105
|
+
@app.template_filter("localtimezone")
|
|
106
|
+
def localtimezone(dt):
|
|
107
|
+
if not dt:
|
|
108
|
+
return ""
|
|
109
|
+
|
|
110
|
+
tz = "UTC"
|
|
111
|
+
if hasattr(g, "user") and g.tenant:
|
|
112
|
+
tz = g.tenant.timezone
|
|
113
|
+
|
|
114
|
+
return dt.astimezone(ZoneInfo(tz))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fmt(dt, fmt):
|
|
118
|
+
dt = localtimezone(dt)
|
|
119
|
+
return dt.strftime(fmt) if dt else ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@app.template_filter("onlydate")
|
|
123
|
+
def onlydate(dt):
|
|
124
|
+
return _fmt(dt, "%Y-%m-%d")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.template_filter("onlytime")
|
|
128
|
+
def onlytime(dt):
|
|
129
|
+
return _fmt(dt, "%H:%M")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.template_filter("localtime")
|
|
133
|
+
def localtime(dt):
|
|
134
|
+
return _fmt(dt, "%Y-%m-%d %H:%M")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.template_filter("shortdatetime")
|
|
138
|
+
def shortdatetime(dt):
|
|
139
|
+
return _fmt(dt, "%b %d %H:%M").lower()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.template_filter("proper")
|
|
143
|
+
def proper(s):
|
|
144
|
+
return s.replace("_", " ").title()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _init_routes(app):
|
|
148
|
+
|
|
149
|
+
@app.route("/manifest.json")
|
|
150
|
+
def manifest():
|
|
151
|
+
return send_from_directory("static", "manifest.json")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _init_error_handlers(app):
|
|
155
|
+
|
|
156
|
+
@app.errorhandler(CSRFError)
|
|
157
|
+
def csrf_error(e):
|
|
158
|
+
return render_template("base/csrf_error.html"), 400
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.errorhandler(404)
|
|
162
|
+
def not_found(e):
|
|
163
|
+
return render_template("base/404.html"), 404
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _user_management(app):
|
|
167
|
+
from blueprints.auth.services import load_user, create_user
|
|
168
|
+
|
|
169
|
+
@app.before_request
|
|
170
|
+
def fetch_user():
|
|
171
|
+
user_id = session.get("user_id")
|
|
172
|
+
|
|
173
|
+
if not user_id:
|
|
174
|
+
g.user = None
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
user = load_user(user_id)
|
|
178
|
+
|
|
179
|
+
if not user:
|
|
180
|
+
g.user = None
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
g.user = user
|
|
184
|
+
|
|
185
|
+
@app.cli.command("initdb")
|
|
186
|
+
@with_appcontext
|
|
187
|
+
def initdb():
|
|
188
|
+
import subprocess
|
|
189
|
+
|
|
190
|
+
required = [
|
|
191
|
+
"DEV_MYSQL_USERNAME",
|
|
192
|
+
"DEV_MYSQL_PASS",
|
|
193
|
+
"DEV_MYSQL_HOST",
|
|
194
|
+
"DEV_MYSQL_NAME"
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
missing = [x for x in required if not os.getenv(x)]
|
|
198
|
+
if missing:
|
|
199
|
+
print(f"Missing env vars: {', '.join(missing)}")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
subprocess.run(["flask", "db", "init"])
|
|
203
|
+
subprocess.run(["flask", "db", "migrate", "-m", "auto init"])
|
|
204
|
+
subprocess.run(["flask", "db", "upgrade"])
|
|
205
|
+
print("Database initialized.")
|
|
206
|
+
|
|
207
|
+
@app.cli.command("superuser")
|
|
208
|
+
@with_appcontext
|
|
209
|
+
def superuser():
|
|
210
|
+
from getpass import getpass
|
|
211
|
+
import os
|
|
212
|
+
|
|
213
|
+
print("Starting application...")
|
|
214
|
+
|
|
215
|
+
load_models()
|
|
216
|
+
|
|
217
|
+
email = input("Email: ").lower().strip()
|
|
218
|
+
username = input("Username: ").lower().strip()
|
|
219
|
+
|
|
220
|
+
while True:
|
|
221
|
+
password = getpass("Password: ")
|
|
222
|
+
password_confirm = getpass("Again: ")
|
|
223
|
+
|
|
224
|
+
if password == password_confirm:
|
|
225
|
+
break
|
|
226
|
+
print("Passwords do not match")
|
|
227
|
+
|
|
228
|
+
res = create_user(
|
|
229
|
+
username=username,
|
|
230
|
+
email=email,
|
|
231
|
+
role="owner",
|
|
232
|
+
password=password
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not res.success:
|
|
236
|
+
print(f"Error: {res.message}")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
print("\nSuperuser created successfully.")
|
|
240
|
+
print("Run: flask run --debug")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from db import db
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
def utc_now():
|
|
6
|
+
return datetime.now(timezone.utc)
|
|
7
|
+
|
|
8
|
+
class User(db.Model):
|
|
9
|
+
__tablename__ = "users"
|
|
10
|
+
|
|
11
|
+
user_id = db.Column(
|
|
12
|
+
db.String(36),
|
|
13
|
+
primary_key=True,
|
|
14
|
+
default=lambda: str(uuid.uuid4())
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
username = db.Column(
|
|
18
|
+
db.String(100),
|
|
19
|
+
unique=True,
|
|
20
|
+
nullable=False
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
role = db.Column(
|
|
24
|
+
db.String(50),
|
|
25
|
+
nullable=False,
|
|
26
|
+
default="user"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
status = db.Column(
|
|
30
|
+
db.String(50),
|
|
31
|
+
nullable=False,
|
|
32
|
+
default="active"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
password_hash = db.Column(
|
|
36
|
+
db.Text,
|
|
37
|
+
nullable=False
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
email = db.Column(
|
|
41
|
+
db.String(255),
|
|
42
|
+
unique=True,
|
|
43
|
+
nullable=False
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
created_at = db.Column(
|
|
47
|
+
db.DateTime(timezone=True),
|
|
48
|
+
default=utc_now
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
last_seen = db.Column(
|
|
52
|
+
db.DateTime(timezone=True),
|
|
53
|
+
default=utc_now
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
failed_login_attempts = db.Column(
|
|
57
|
+
db.Integer,
|
|
58
|
+
default=0
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
locked_until = db.Column(
|
|
62
|
+
db.DateTime(timezone=True)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
deactivated_at = db.Column(
|
|
66
|
+
db.DateTime(timezone=True)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
deleted_at = db.Column(
|
|
70
|
+
db.DateTime(timezone=True)
|
|
71
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from flask import render_template, redirect, url_for, request, Blueprint, flash, session
|
|
2
|
+
from tools import genflash
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
auth_bp = Blueprint(
|
|
6
|
+
"auth_bp",
|
|
7
|
+
__name__,
|
|
8
|
+
template_folder="templates",
|
|
9
|
+
static_folder="static"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
@auth_bp.route("/login", methods=["GET", "POST"])
|
|
13
|
+
def login():
|
|
14
|
+
|
|
15
|
+
if request.method == 'POST':
|
|
16
|
+
from blueprints.auth.services import authenticate_user
|
|
17
|
+
|
|
18
|
+
res = authenticate_user(
|
|
19
|
+
email=request.form.get("email", "").strip().lower(),
|
|
20
|
+
password=request.form.get("password", "")
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if not res.success:
|
|
24
|
+
msg, cat = genflash(res.message, "error")
|
|
25
|
+
flash(msg ,cat)
|
|
26
|
+
return redirect(url_for("auth_bp.login"))
|
|
27
|
+
|
|
28
|
+
session["user_id"] = res.data["user_id"]
|
|
29
|
+
msg, cat = genflash("Welcome", "chat")
|
|
30
|
+
flash(msg, cat)
|
|
31
|
+
return redirect(url_for("core_bp.home"))
|
|
32
|
+
|
|
33
|
+
return render_template("auth/login.html")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@auth_bp.post("/logout")
|
|
37
|
+
def logout():
|
|
38
|
+
session.clear()
|
|
39
|
+
msg, cat = genflash("Bye bye!", "chat")
|
|
40
|
+
flash(msg, cat)
|
|
41
|
+
return redirect(url_for("auth_bp.login"))
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
from werkzeug.security import generate_password_hash, check_password_hash
|
|
2
|
+
from blueprints.core.error_logging import log_error
|
|
3
|
+
from datetime import datetime, timezone, timedelta
|
|
4
|
+
from blueprints.auth.roles import roles
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, Any
|
|
7
|
+
from models import User
|
|
8
|
+
from flask import g
|
|
9
|
+
from db import db
|
|
10
|
+
import uuid
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Result:
|
|
16
|
+
success: bool
|
|
17
|
+
data: Optional[Any] = None
|
|
18
|
+
message: str = ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def utc_now():
|
|
22
|
+
return datetime.now(timezone.utc)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def valid_email_pattern(email: str) -> bool:
|
|
26
|
+
pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
|
|
27
|
+
return bool(re.match(pattern, email))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_password_strength(password: str) -> Result:
|
|
31
|
+
if len(password) < 8:
|
|
32
|
+
return Result(False, message="Password must be at least 8 characters long.")
|
|
33
|
+
|
|
34
|
+
if not re.search(r"[A-Z]", password):
|
|
35
|
+
return Result(False, message="Password must contain at least one uppercase letter.")
|
|
36
|
+
|
|
37
|
+
if not re.search(r"[a-z]", password):
|
|
38
|
+
return Result(False, message="Password must contain at least one lowercase letter.")
|
|
39
|
+
|
|
40
|
+
if not re.search(r"[^A-Za-z0-9]", password):
|
|
41
|
+
return Result(False, message="Password must contain at least one special character.")
|
|
42
|
+
|
|
43
|
+
return Result(True)
|
|
44
|
+
|
|
45
|
+
def username_exists(username: str) -> bool:
|
|
46
|
+
return User.query.filter_by(username=username).first() is not None
|
|
47
|
+
|
|
48
|
+
def validate_role(role: str) -> bool:
|
|
49
|
+
return role.strip().lower() in {r.lower() for r in roles()}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def email_exists(email: str) -> bool:
|
|
53
|
+
return User.query.filter_by(email=email).first() is not None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_user_creation(username: str, email: str, role: str, password: str) -> Result:
|
|
57
|
+
|
|
58
|
+
if username_exists(username):
|
|
59
|
+
return Result(False, message="Username already taken")
|
|
60
|
+
|
|
61
|
+
if not validate_role(role=role):
|
|
62
|
+
return Result(False, message="Invalid role")
|
|
63
|
+
|
|
64
|
+
if not valid_email_pattern(email):
|
|
65
|
+
return Result(False, message="Invalid email address")
|
|
66
|
+
|
|
67
|
+
if email_exists(email):
|
|
68
|
+
return Result(False, message="Email already registered")
|
|
69
|
+
|
|
70
|
+
password_check = validate_password_strength(password)
|
|
71
|
+
if not password_check.success:
|
|
72
|
+
return password_check
|
|
73
|
+
|
|
74
|
+
return Result(True)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def create_user(username: str, email: str, role: str, password: str) -> Result:
|
|
78
|
+
|
|
79
|
+
validation = validate_user_creation(username, email, role, password)
|
|
80
|
+
if not validation.success:
|
|
81
|
+
return validation
|
|
82
|
+
|
|
83
|
+
user = User(
|
|
84
|
+
user_id=str(uuid.uuid4()),
|
|
85
|
+
username=username,
|
|
86
|
+
email=email,
|
|
87
|
+
role=role,
|
|
88
|
+
password_hash=generate_password_hash(password),
|
|
89
|
+
created_at=utc_now()
|
|
90
|
+
)
|
|
91
|
+
try:
|
|
92
|
+
db.session.add(user)
|
|
93
|
+
db.session.commit()
|
|
94
|
+
|
|
95
|
+
return Result(True, message="User created successfully")
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
log_error(
|
|
99
|
+
user_id=getattr(g, "user_id", None),
|
|
100
|
+
function="create_user",
|
|
101
|
+
action="Creating user",
|
|
102
|
+
exception=e
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return Result(False, message="An error occurred when creating the user")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def authenticate_user(email, password):
|
|
109
|
+
user = User.query.filter_by(email=email).first()
|
|
110
|
+
|
|
111
|
+
if not user:
|
|
112
|
+
return Result(False, message="invalid credentials")
|
|
113
|
+
|
|
114
|
+
now = datetime.now(timezone.utc)
|
|
115
|
+
|
|
116
|
+
if user.status == "deleted":
|
|
117
|
+
delete_at = user.deactivated_at + timedelta(days=30)
|
|
118
|
+
if delete_at < now:
|
|
119
|
+
message = "This account and all its data was deleted permanently. Please create a new one"
|
|
120
|
+
else:
|
|
121
|
+
message = f"This account will be permanently deleted on {delete_at.strftime('%Y-%m-%d %H:%M UTC')}."
|
|
122
|
+
return Result(False, message=message)
|
|
123
|
+
|
|
124
|
+
now = datetime.now(timezone.utc)
|
|
125
|
+
|
|
126
|
+
if user.locked_until and user.locked_until > now:
|
|
127
|
+
return Result(False, message="Too many attempts. Try again later.")
|
|
128
|
+
|
|
129
|
+
if not check_password_hash(user.password_hash, password):
|
|
130
|
+
user.failed_login_attempts = (user.failed_login_attempts or 0) + 1
|
|
131
|
+
|
|
132
|
+
if user.failed_login_attempts >= 5:
|
|
133
|
+
user.locked_until = now + timedelta(minutes=10)
|
|
134
|
+
|
|
135
|
+
db.session.commit()
|
|
136
|
+
|
|
137
|
+
return Result(False, message="Invalid credentials")
|
|
138
|
+
|
|
139
|
+
user.failed_login_attempts = 0
|
|
140
|
+
user.locked_until = None
|
|
141
|
+
db.session.commit()
|
|
142
|
+
|
|
143
|
+
return Result(
|
|
144
|
+
True,
|
|
145
|
+
data={"user_id": user.user_id},
|
|
146
|
+
message="User authenticated successfully"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_user(user_id):
|
|
151
|
+
user = db.session.query(
|
|
152
|
+
User.user_id,
|
|
153
|
+
User.username,
|
|
154
|
+
User.email,
|
|
155
|
+
User.role,
|
|
156
|
+
User.status,
|
|
157
|
+
User.created_at
|
|
158
|
+
).filter_by(user_id=user_id).first()
|
|
159
|
+
if not user:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
return user
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from db import db
|
|
2
|
+
from blueprints.core.models import Error
|
|
3
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
4
|
+
from flask import g
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def log_error(
|
|
9
|
+
user_id: str,
|
|
10
|
+
function: str,
|
|
11
|
+
action: str,
|
|
12
|
+
exception: Exception
|
|
13
|
+
):
|
|
14
|
+
try:
|
|
15
|
+
db.session.add(
|
|
16
|
+
Error(
|
|
17
|
+
user_id=user_id,
|
|
18
|
+
function=function,
|
|
19
|
+
action=action,
|
|
20
|
+
error_message=str(exception),
|
|
21
|
+
traceback=traceback.format_exc()
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
db.session.commit()
|
|
25
|
+
|
|
26
|
+
except SQLAlchemyError:
|
|
27
|
+
db.session.rollback()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from db import db
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def utc_now():
|
|
7
|
+
return datetime.now(timezone.utc)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Error(db.Model):
|
|
11
|
+
__tablename__="errors"
|
|
12
|
+
|
|
13
|
+
error_id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
14
|
+
user_id = db.Column(db.String(36), db.ForeignKey("users.user_id"))
|
|
15
|
+
function = db.Column(db.String(100))
|
|
16
|
+
action = db.Column(db.String(100))
|
|
17
|
+
error = db.Column(db.Text)
|
|
18
|
+
traceback = db.Column(db.Text)
|
|
19
|
+
created_at = db.Column(db.DateTime(timezone=True), default=utc_now, index=True)
|
|
20
|
+
|
|
21
|
+
user = db.relationship("User")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from flask import render_template, redirect, url_for, Blueprint, g
|
|
2
|
+
from wrappers import login_required
|
|
3
|
+
|
|
4
|
+
core_bp = Blueprint(
|
|
5
|
+
"core_bp",
|
|
6
|
+
__name__,
|
|
7
|
+
template_folder="templates",
|
|
8
|
+
static_folder="static"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
@core_bp.route("/")
|
|
12
|
+
@login_required
|
|
13
|
+
def home():
|
|
14
|
+
|
|
15
|
+
context = {
|
|
16
|
+
"selected": "home",
|
|
17
|
+
"user": g.user
|
|
18
|
+
}
|
|
19
|
+
return render_template("/core/home.html", **context)
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from flask import render_template, redirect, url_for, Blueprint, g
|
|
2
|
+
from wrappers import login_required
|
|
3
|
+
|
|
4
|
+
settings_bp = Blueprint(
|
|
5
|
+
"settings_bp",
|
|
6
|
+
__name__,
|
|
7
|
+
template_folder="templates",
|
|
8
|
+
static_folder="static"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
@settings_bp.route("/settings")
|
|
12
|
+
@login_required
|
|
13
|
+
def settings():
|
|
14
|
+
context = {
|
|
15
|
+
"selected": "settings",
|
|
16
|
+
"user": g.user,
|
|
17
|
+
|
|
18
|
+
}
|
|
19
|
+
return render_template("/settings/general.html", **context)
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
class BaseConfig:
|
|
5
|
+
SECRET_KEY = os.getenv("SECRET_KEY")
|
|
6
|
+
|
|
7
|
+
# Fail fast instead of silently breaking later
|
|
8
|
+
if not SECRET_KEY:
|
|
9
|
+
raise ValueError("SECRET_KEY is missing")
|
|
10
|
+
|
|
11
|
+
CRKEY = os.getenv("CRKEY")
|
|
12
|
+
|
|
13
|
+
PERMANENT_SESSION_LIFETIME = timedelta(days=365)
|
|
14
|
+
SESSION_TYPE = "filesystem"
|
|
15
|
+
SESSION_USE_SIGNER = True
|
|
16
|
+
SESSION_FILE_DIR = os.path.join(os.getcwd(), "flask_session")
|
|
17
|
+
|
|
18
|
+
CACHE_TYPE = "SimpleCache"
|
|
19
|
+
CACHE_DEFAULT_TIMEOUT = 300
|
|
20
|
+
|
|
21
|
+
WTF_CSRF_TIME_LIMIT = 3 * 60 * 60
|
|
22
|
+
|
|
23
|
+
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConfigProd(BaseConfig):
|
|
27
|
+
SESSION_COOKIE_SECURE = True
|
|
28
|
+
|
|
29
|
+
SQLALCHEMY_DATABASE_URI = (
|
|
30
|
+
f"mysql+pymysql://{os.getenv('PROD_MYSQL_USERNAME')}:"
|
|
31
|
+
f"{os.getenv('PROD_MYSQL_PASS')}@"
|
|
32
|
+
f"{os.getenv('PROD_MYSQL_HOST')}/"
|
|
33
|
+
f"{os.getenv('PROD_MYSQL_NAME')}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ConfigTest(BaseConfig):
|
|
38
|
+
SESSION_COOKIE_SECURE = False
|
|
39
|
+
CACHE_DEFAULT_TIMEOUT = 60
|
|
40
|
+
|
|
41
|
+
SQLALCHEMY_DATABASE_URI = (
|
|
42
|
+
f"mysql+pymysql://{os.getenv('DEV_MYSQL_USERNAME')}:"
|
|
43
|
+
f"{os.getenv('DEV_MYSQL_PASS')}@"
|
|
44
|
+
f"{os.getenv('DEV_MYSQL_HOST')}/"
|
|
45
|
+
f"{os.getenv('DEV_MYSQL_NAME')}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
config_dict = {
|
|
50
|
+
"prod": ConfigProd,
|
|
51
|
+
"test": ConfigTest,
|
|
52
|
+
"default": ConfigTest
|
|
53
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
def genflash(message: str, category: str):
|
|
2
|
+
"""
|
|
3
|
+
Accepted categories:
|
|
4
|
+
- bug
|
|
5
|
+
- error
|
|
6
|
+
- warning
|
|
7
|
+
- ok
|
|
8
|
+
- safe
|
|
9
|
+
- win
|
|
10
|
+
- timeout
|
|
11
|
+
- wait
|
|
12
|
+
- announce
|
|
13
|
+
- upload
|
|
14
|
+
- save
|
|
15
|
+
- delete
|
|
16
|
+
- thumbsup
|
|
17
|
+
- thumbsdown
|
|
18
|
+
- chat
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
cats = {
|
|
22
|
+
'bug': {'icon': 'bug-outline', 'class': 'text-amber-400'},
|
|
23
|
+
'error': {'icon': 'alert-circle-outline', 'class': 'text-red-400'},
|
|
24
|
+
'warning': {'icon': 'warning-outline', 'class': 'text-amber-400'},
|
|
25
|
+
'ok': {'icon': 'checkmark-circle-outline', 'class': 'text-green-400'},
|
|
26
|
+
'safe': {'icon': 'shield-checkmark-outline', 'class': 'text-teal-400'},
|
|
27
|
+
'win': {'icon': 'medal-outline', 'class': 'text-sky-400'},
|
|
28
|
+
'timeout': {'icon': 'alarm-outline', 'class': 'text-amber-400'},
|
|
29
|
+
'wait': {'icon': 'hourglass-outline', 'class': 'text-amber-400'},
|
|
30
|
+
'announce': {'icon': 'megaphone-outline', 'class': 'text-teal-400'},
|
|
31
|
+
'upload': {'icon': 'cloud-upload-outline', 'class': 'text-white'},
|
|
32
|
+
'save': {'icon': 'save-outline', 'class': 'text-sky-400'},
|
|
33
|
+
'delete': {'icon': 'trash-outline', 'class': 'text-red-400'},
|
|
34
|
+
'thumbsup': {'icon': 'thumbs-up-outline', 'class': 'text-green-400'},
|
|
35
|
+
'thumbsdown': {'icon': 'thumbs-down-outline', 'class': 'text-red-400'},
|
|
36
|
+
'chat': {'icon': 'chatbubble-ellipses-outline', 'class': 'text-sky-400'},
|
|
37
|
+
}
|
|
38
|
+
if category not in cats:
|
|
39
|
+
raise ValueError("Invalid category")
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"text": message,
|
|
43
|
+
"icon": cats[category]['icon'],
|
|
44
|
+
"icon_class": cats[category]['class'],
|
|
45
|
+
}, category
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from flask import session, redirect, url_for, flash, g
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from tools import genflash
|
|
4
|
+
|
|
5
|
+
def login_required(f):
|
|
6
|
+
@wraps(f)
|
|
7
|
+
def decorated_function(*args, **kwargs):
|
|
8
|
+
if 'user_id' not in session or not g.user:
|
|
9
|
+
msg, cat = genflash("You must log in to visit this page", "error")
|
|
10
|
+
flash(msg, cat)
|
|
11
|
+
return redirect(url_for('auth_bp.login'))
|
|
12
|
+
return f(*args, **kwargs)
|
|
13
|
+
return decorated_function
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lombik
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flask scaffold engine
|
|
5
|
+
Author: David Plajner
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: Flask
|
|
9
|
+
Requires-Dist: Flask-SQLAlchemy
|
|
10
|
+
Requires-Dist: Flask-Migrate
|
|
11
|
+
Requires-Dist: Flask-WTF
|
|
12
|
+
Requires-Dist: Flask-Session
|
|
13
|
+
Requires-Dist: python-dotenv
|
|
14
|
+
Requires-Dist: click
|
|
15
|
+
Requires-Dist: SQLAlchemy
|
|
16
|
+
Requires-Dist: Werkzeug
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Lombik
|
|
20
|
+
#### A scaffold engine for Flask, the greatest framework of all times.
|
|
21
|
+
|
|
22
|
+
This readme will serve perfectly as a self documentation file as I go, and at the end of it I will re-write it with the final details.
|
|
23
|
+
That means that if you see this, either my company cleersoftware has taken off and you might work with me on enhancing the scaffold for our apps.
|
|
24
|
+
Another possibility is that lombik took off, and you are reading the early commits. Have fun, there is probably a lot of them.
|
|
25
|
+
Anyways, I'm gonan actually start on explaning / plannign how thigns will work.
|
|
26
|
+
|
|
27
|
+
> Install lombik
|
|
28
|
+
Run `pip install lombik`
|
|
29
|
+
|
|
30
|
+
> Create app
|
|
31
|
+
To create an app called *myapp*, run `lombik createapp myapp` then `cd myapp`
|
|
32
|
+
This creates the entire structure of your application in one go and you can run `flask run --debug`
|
|
33
|
+
|
|
34
|
+
> Create superuser
|
|
35
|
+
Lombik comes with MySQL as a default db. In the generated .env file you'll find the credentials for local development.
|
|
36
|
+
Replace those placeholders with your actual dev credentials and production if you have it already.
|
|
37
|
+
When done, initialize the database by running `flask initdb` and then you can run `flask superuser` to create your owner account.
|
|
38
|
+
With this, you'll be able to log in to your app and start developing from there.
|
|
39
|
+
|
|
40
|
+
This is a quite simple auth system, in fact it is simple by design and meant to be replaced for production apps with something more robust.
|
|
41
|
+
|
|
42
|
+
> Features and benefits
|
|
43
|
+
On top of the structural guidance, lombik comes with a few built in goodies.
|
|
44
|
+
|
|
45
|
+
- User authentication and session handling
|
|
46
|
+
- CSRF / session expiry
|
|
47
|
+
- Error handling
|
|
48
|
+
- Structure
|
|
49
|
+
- Template filters for frontend development
|
|
50
|
+
- Pre-imported libraries
|
|
51
|
+
- General templates for desktop and mobile
|
|
52
|
+
|
|
53
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
lombik/__init__.py
|
|
5
|
+
lombik/cli.py
|
|
6
|
+
lombik.egg-info/PKG-INFO
|
|
7
|
+
lombik.egg-info/SOURCES.txt
|
|
8
|
+
lombik.egg-info/dependency_links.txt
|
|
9
|
+
lombik.egg-info/entry_points.txt
|
|
10
|
+
lombik.egg-info/requires.txt
|
|
11
|
+
lombik.egg-info/top_level.txt
|
|
12
|
+
lombik/templates/__init__.py
|
|
13
|
+
lombik/templates/createapp/app.py
|
|
14
|
+
lombik/templates/createapp/config.py
|
|
15
|
+
lombik/templates/createapp/db.py
|
|
16
|
+
lombik/templates/createapp/tools.py
|
|
17
|
+
lombik/templates/createapp/wrappers.py
|
|
18
|
+
lombik/templates/createapp/blueprints/auth/models.py
|
|
19
|
+
lombik/templates/createapp/blueprints/auth/roles.py
|
|
20
|
+
lombik/templates/createapp/blueprints/auth/routes.py
|
|
21
|
+
lombik/templates/createapp/blueprints/auth/services.py
|
|
22
|
+
lombik/templates/createapp/blueprints/core/error_logging.py
|
|
23
|
+
lombik/templates/createapp/blueprints/core/models.py
|
|
24
|
+
lombik/templates/createapp/blueprints/core/routes.py
|
|
25
|
+
lombik/templates/createapp/blueprints/settings/modesl.py
|
|
26
|
+
lombik/templates/createapp/blueprints/settings/routes.py
|
|
27
|
+
lombik/templates/createapp/blueprints/settings/services.py
|
|
28
|
+
lombik/templates/createapp/models/__init__.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
lombik
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lombik"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Flask scaffold engine"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name="David Plajner" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"Flask",
|
|
16
|
+
"Flask-SQLAlchemy",
|
|
17
|
+
"Flask-Migrate",
|
|
18
|
+
"Flask-WTF",
|
|
19
|
+
"Flask-Session",
|
|
20
|
+
"python-dotenv",
|
|
21
|
+
"click",
|
|
22
|
+
"SQLAlchemy",
|
|
23
|
+
"Werkzeug"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
lombik = "lombik.cli:cli"
|
lombik-0.1.0/setup.cfg
ADDED