jdm-electron-flask 1.0.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.
- jdm_electron_flask-1.0.0/PKG-INFO +183 -0
- jdm_electron_flask-1.0.0/README.md +157 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/__init__.py +35 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/core/app.py +154 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/core/blueprint.py +80 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/core/config.py +31 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/core/event.py +45 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/utils/printer.py +69 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/utils/responses.py +15 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask/utils/validators.py +106 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask.egg-info/PKG-INFO +183 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask.egg-info/SOURCES.txt +15 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask.egg-info/dependency_links.txt +1 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask.egg-info/requires.txt +4 -0
- jdm_electron_flask-1.0.0/jdm_electron_flask.egg-info/top_level.txt +1 -0
- jdm_electron_flask-1.0.0/pyproject.toml +39 -0
- jdm_electron_flask-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jdm-electron-flask
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The Python backbone for jdm-electron-flask desktop apps
|
|
5
|
+
Author-email: JDM-Github <jdmaster888@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JDM-Github/jdm-electron-flask
|
|
8
|
+
Project-URL: Repository, https://github.com/JDM-Github/jdm-electron-flask
|
|
9
|
+
Project-URL: Issues, https://github.com/JDM-Github/jdm-electron-flask/issues
|
|
10
|
+
Keywords: flask,electron,desktop,socketio,jdm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: flask>=3.0.0
|
|
23
|
+
Requires-Dist: flask-cors>=4.0.0
|
|
24
|
+
Requires-Dist: flask-socketio>=5.3.0
|
|
25
|
+
Requires-Dist: simple-websocket>=1.0.0
|
|
26
|
+
|
|
27
|
+
# jdm-electron-flask (Python)
|
|
28
|
+
|
|
29
|
+
> The Python backbone for [`jdm-electron-flask`](https://github.com/JDM-Github/jdm-electron-flask) desktop apps.
|
|
30
|
+
|
|
31
|
+
This package ships inside every project scaffolded by the `jdm-cli electron-flask` plugin. It provides the app factory, dynamic API registration, SocketIO setup, and environment-aware logging — so you build features, not boilerplate.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
You don't install this manually. It's included in the scaffolded `requirements.txt`:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
jdm-electron-flask==1.0.0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If you need it standalone:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install jdm-electron-flask
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## What it provides
|
|
52
|
+
|
|
53
|
+
### `create_app`
|
|
54
|
+
The Flask app factory. Handles config loading, SocketIO init, blueprint auto-discovery, and the React SPA catch-all route.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# run.py
|
|
58
|
+
from jdm_electron_flask import create_app, get_socketio
|
|
59
|
+
|
|
60
|
+
app = create_app()
|
|
61
|
+
socketio = get_socketio()
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `JDMBlueprint`
|
|
68
|
+
Extended Blueprint with built-in response helpers.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from jdm_electron_flask import JDMBlueprint
|
|
72
|
+
|
|
73
|
+
health_bp = JDMBlueprint("health", __name__)
|
|
74
|
+
|
|
75
|
+
@health_bp.route("/health")
|
|
76
|
+
def health():
|
|
77
|
+
return health_bp.success({"status": "ok"})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `JDMEvents`
|
|
81
|
+
Base class for organizing socket event handlers.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from jdm_electron_flask import JDMEvents
|
|
85
|
+
|
|
86
|
+
class ConnectEvents(JDMEvents):
|
|
87
|
+
def register(self):
|
|
88
|
+
@self.socketio.on("connect")
|
|
89
|
+
def on_connect():
|
|
90
|
+
self.on_connect()
|
|
91
|
+
|
|
92
|
+
@self.socketio.on("ping_server")
|
|
93
|
+
def on_ping(data):
|
|
94
|
+
self.emit("pong_client", {"echo": data})
|
|
95
|
+
|
|
96
|
+
ConnectEvents().register()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `Printer`
|
|
100
|
+
Environment-aware logger. Prints in development, writes to `logs/<date>.log` in production/deployed.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from jdm_electron_flask import Printer
|
|
104
|
+
|
|
105
|
+
Printer.success("Blueprint registered")
|
|
106
|
+
Printer.warn("Something is off")
|
|
107
|
+
Printer.error("Something broke")
|
|
108
|
+
Printer.info("Just FYI")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Response helpers
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from jdm_electron_flask import success, error, paginate
|
|
115
|
+
|
|
116
|
+
return success({"user": "JDM"})
|
|
117
|
+
return error("Not found", status=404)
|
|
118
|
+
return paginate(items, total=100, page=1, per_page=10)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Validators
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from jdm_electron_flask import validate_fields
|
|
125
|
+
|
|
126
|
+
valid, msg, data = validate_fields(["name", "email"])
|
|
127
|
+
if not valid:
|
|
128
|
+
return error(msg)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Dynamic API registration
|
|
134
|
+
|
|
135
|
+
Routes are controlled by `config/api.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"health": {
|
|
140
|
+
"link": "/api",
|
|
141
|
+
"masterEnabled": true
|
|
142
|
+
},
|
|
143
|
+
"users": {
|
|
144
|
+
"link": "/api/users",
|
|
145
|
+
"masterEnabled": true,
|
|
146
|
+
"disabledOnProduction": false,
|
|
147
|
+
"disabledOnDeployed": false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Adding a new route:
|
|
153
|
+
1. Add entry to `config/api.json`
|
|
154
|
+
2. Create `app/api/users.py` with a `users_bp` blueprint
|
|
155
|
+
|
|
156
|
+
That's it. No touching `__init__.py` ever again.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Environment modes
|
|
161
|
+
|
|
162
|
+
| `FLASK_ENV` | Behavior |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `development` | Debug on, console logging, all routes enabled |
|
|
165
|
+
| `production` | Debug off, file logging, respects `disabledOnProduction` |
|
|
166
|
+
| `deployed` | Debug off, file logging, respects `disabledOnDeployed` |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Companion
|
|
171
|
+
|
|
172
|
+
This package is the Python half of the `jdm-electron-flask` ecosystem:
|
|
173
|
+
|
|
174
|
+
| Package | Role |
|
|
175
|
+
|---|---|
|
|
176
|
+
| [`jdm-electron-flask`](https://www.npmjs.com/package/jdm-electron-flask) (npm) | CLI plugin — scaffold, build, compile |
|
|
177
|
+
| `jdm-electron-flask` (PyPI) | Backend library — app factory, blueprints, sockets |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT © [JDM-Github](https://github.com/JDM-Github)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# jdm-electron-flask (Python)
|
|
2
|
+
|
|
3
|
+
> The Python backbone for [`jdm-electron-flask`](https://github.com/JDM-Github/jdm-electron-flask) desktop apps.
|
|
4
|
+
|
|
5
|
+
This package ships inside every project scaffolded by the `jdm-cli electron-flask` plugin. It provides the app factory, dynamic API registration, SocketIO setup, and environment-aware logging — so you build features, not boilerplate.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
You don't install this manually. It's included in the scaffolded `requirements.txt`:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
jdm-electron-flask==1.0.0
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If you need it standalone:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install jdm-electron-flask
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## What it provides
|
|
26
|
+
|
|
27
|
+
### `create_app`
|
|
28
|
+
The Flask app factory. Handles config loading, SocketIO init, blueprint auto-discovery, and the React SPA catch-all route.
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# run.py
|
|
32
|
+
from jdm_electron_flask import create_app, get_socketio
|
|
33
|
+
|
|
34
|
+
app = create_app()
|
|
35
|
+
socketio = get_socketio()
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `JDMBlueprint`
|
|
42
|
+
Extended Blueprint with built-in response helpers.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from jdm_electron_flask import JDMBlueprint
|
|
46
|
+
|
|
47
|
+
health_bp = JDMBlueprint("health", __name__)
|
|
48
|
+
|
|
49
|
+
@health_bp.route("/health")
|
|
50
|
+
def health():
|
|
51
|
+
return health_bp.success({"status": "ok"})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `JDMEvents`
|
|
55
|
+
Base class for organizing socket event handlers.
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from jdm_electron_flask import JDMEvents
|
|
59
|
+
|
|
60
|
+
class ConnectEvents(JDMEvents):
|
|
61
|
+
def register(self):
|
|
62
|
+
@self.socketio.on("connect")
|
|
63
|
+
def on_connect():
|
|
64
|
+
self.on_connect()
|
|
65
|
+
|
|
66
|
+
@self.socketio.on("ping_server")
|
|
67
|
+
def on_ping(data):
|
|
68
|
+
self.emit("pong_client", {"echo": data})
|
|
69
|
+
|
|
70
|
+
ConnectEvents().register()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `Printer`
|
|
74
|
+
Environment-aware logger. Prints in development, writes to `logs/<date>.log` in production/deployed.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from jdm_electron_flask import Printer
|
|
78
|
+
|
|
79
|
+
Printer.success("Blueprint registered")
|
|
80
|
+
Printer.warn("Something is off")
|
|
81
|
+
Printer.error("Something broke")
|
|
82
|
+
Printer.info("Just FYI")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Response helpers
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from jdm_electron_flask import success, error, paginate
|
|
89
|
+
|
|
90
|
+
return success({"user": "JDM"})
|
|
91
|
+
return error("Not found", status=404)
|
|
92
|
+
return paginate(items, total=100, page=1, per_page=10)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Validators
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from jdm_electron_flask import validate_fields
|
|
99
|
+
|
|
100
|
+
valid, msg, data = validate_fields(["name", "email"])
|
|
101
|
+
if not valid:
|
|
102
|
+
return error(msg)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Dynamic API registration
|
|
108
|
+
|
|
109
|
+
Routes are controlled by `config/api.json`:
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"health": {
|
|
114
|
+
"link": "/api",
|
|
115
|
+
"masterEnabled": true
|
|
116
|
+
},
|
|
117
|
+
"users": {
|
|
118
|
+
"link": "/api/users",
|
|
119
|
+
"masterEnabled": true,
|
|
120
|
+
"disabledOnProduction": false,
|
|
121
|
+
"disabledOnDeployed": false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Adding a new route:
|
|
127
|
+
1. Add entry to `config/api.json`
|
|
128
|
+
2. Create `app/api/users.py` with a `users_bp` blueprint
|
|
129
|
+
|
|
130
|
+
That's it. No touching `__init__.py` ever again.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Environment modes
|
|
135
|
+
|
|
136
|
+
| `FLASK_ENV` | Behavior |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `development` | Debug on, console logging, all routes enabled |
|
|
139
|
+
| `production` | Debug off, file logging, respects `disabledOnProduction` |
|
|
140
|
+
| `deployed` | Debug off, file logging, respects `disabledOnDeployed` |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Companion
|
|
145
|
+
|
|
146
|
+
This package is the Python half of the `jdm-electron-flask` ecosystem:
|
|
147
|
+
|
|
148
|
+
| Package | Role |
|
|
149
|
+
|---|---|
|
|
150
|
+
| [`jdm-electron-flask`](https://www.npmjs.com/package/jdm-electron-flask) (npm) | CLI plugin — scaffold, build, compile |
|
|
151
|
+
| `jdm-electron-flask` (PyPI) | Backend library — app factory, blueprints, sockets |
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT © [JDM-Github](https://github.com/JDM-Github)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jdm-electron-flask-py
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
The Python backbone for jdm-electron-flask projects.
|
|
5
|
+
|
|
6
|
+
Provides app factory, dynamic API registration, SocketIO setup,
|
|
7
|
+
and environment-aware logging — so you build features, not boilerplate.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "1.0.0"
|
|
11
|
+
__author__ = "JDM-Github"
|
|
12
|
+
|
|
13
|
+
from jdm_electron_flask.core.app import create_app, get_socketio
|
|
14
|
+
from jdm_electron_flask.core.blueprint import JDMBlueprint
|
|
15
|
+
from jdm_electron_flask.core.event import JDMEvent
|
|
16
|
+
from jdm_electron_flask.utils.printer import Printer
|
|
17
|
+
from jdm_electron_flask.utils.responses import success, error
|
|
18
|
+
from jdm_electron_flask.utils.validators import require_access, validate_json
|
|
19
|
+
from jdm_electron_flask.core.config import JDMConfig, JDMDevelopmentConfig, JDMProductionConfig, JDMDeployedConfig
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"create_app",
|
|
23
|
+
"get_socketio",
|
|
24
|
+
"JDMBlueprint",
|
|
25
|
+
"JDMEvent",
|
|
26
|
+
"Printer",
|
|
27
|
+
"success",
|
|
28
|
+
"error",
|
|
29
|
+
"require_access",
|
|
30
|
+
"validate_json",
|
|
31
|
+
"JDMConfig",
|
|
32
|
+
"JDMDevelopmentConfig",
|
|
33
|
+
"JDMProductionConfig",
|
|
34
|
+
"JDMDeployedConfig"
|
|
35
|
+
]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
from flask import Flask, send_from_directory
|
|
2
|
+
from flask_cors import CORS
|
|
3
|
+
from flask_socketio import SocketIO
|
|
4
|
+
import importlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import inspect
|
|
8
|
+
from jdm_electron_flask.utils.printer import Printer
|
|
9
|
+
_socketio = SocketIO()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_socketio() -> SocketIO:
|
|
13
|
+
"""Return the shared SocketIO instance."""
|
|
14
|
+
return _socketio
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_config(config_name: str, app: Flask):
|
|
18
|
+
"""Load Flask config based on environment name."""
|
|
19
|
+
try:
|
|
20
|
+
if config_name == "production":
|
|
21
|
+
from app.config import ProductionConfig # type: ignore
|
|
22
|
+
app.config.from_object(ProductionConfig)
|
|
23
|
+
elif config_name == "deployed":
|
|
24
|
+
from app.config import DeployedConfig # type: ignore
|
|
25
|
+
app.config.from_object(DeployedConfig)
|
|
26
|
+
else:
|
|
27
|
+
from app.config import DevelopmentConfig # type: ignore
|
|
28
|
+
app.config.from_object(DevelopmentConfig)
|
|
29
|
+
except ImportError:
|
|
30
|
+
Printer.warn(f"Config class for '{config_name}' not found — using Flask defaults")
|
|
31
|
+
|
|
32
|
+
def _load_blueprints(app: Flask, env: str):
|
|
33
|
+
import inspect
|
|
34
|
+
from jdm_electron_flask import JDMBlueprint
|
|
35
|
+
|
|
36
|
+
api_config_path = os.path.join(os.getcwd(), "config", "api.json")
|
|
37
|
+
if not os.path.exists(api_config_path):
|
|
38
|
+
Printer.warn("config/api.json not found — no blueprints registered")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
with open(api_config_path, "r") as f:
|
|
42
|
+
api_config = json.load(f)
|
|
43
|
+
|
|
44
|
+
for name, config in api_config.items():
|
|
45
|
+
if not config.get("masterEnabled", True):
|
|
46
|
+
Printer.warn(f"[{name}] skipped (masterEnabled: false)")
|
|
47
|
+
continue
|
|
48
|
+
if env == "production" and config.get("disabledOnProduction", False):
|
|
49
|
+
Printer.warn(f"[{name}] skipped (disabledOnProduction: true)")
|
|
50
|
+
continue
|
|
51
|
+
if env == "deployed" and config.get("disabledOnDeployed", False):
|
|
52
|
+
Printer.warn(f"[{name}] skipped (disabledOnDeployed: true)")
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
module = importlib.import_module(f"app.api.{name}")
|
|
57
|
+
blueprint = None
|
|
58
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
59
|
+
if issubclass(obj, JDMBlueprint) and obj is not JDMBlueprint:
|
|
60
|
+
blueprint = obj()
|
|
61
|
+
break
|
|
62
|
+
if blueprint is None:
|
|
63
|
+
blueprint = getattr(module, f"{name}_bp", None)
|
|
64
|
+
|
|
65
|
+
if blueprint is None:
|
|
66
|
+
Printer.error(f"[{name}] no JDMBlueprint subclass or '{name}_bp' found — skipped")
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
app.register_blueprint(blueprint, url_prefix=config["link"])
|
|
70
|
+
Printer.success(f"[{name}] registered at {config['link']}")
|
|
71
|
+
|
|
72
|
+
except ModuleNotFoundError as e:
|
|
73
|
+
if f"app.api.{name}" in str(e):
|
|
74
|
+
Printer.error(f"[{name}] module app/api/{name}.py not found — skipped")
|
|
75
|
+
else:
|
|
76
|
+
Printer.error(f"[{name}] failed to import — missing dependency: {e}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
Printer.error(f"[{name}] unexpected error — {e}")
|
|
79
|
+
|
|
80
|
+
def _load_events():
|
|
81
|
+
from jdm_electron_flask import JDMEvent
|
|
82
|
+
event_dir = os.path.join(os.getcwd(), "app", "event")
|
|
83
|
+
if not os.path.exists(event_dir):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
for filename in os.listdir(event_dir):
|
|
87
|
+
if filename.endswith(".py") and not filename.startswith("_"):
|
|
88
|
+
module_name = filename[:-3]
|
|
89
|
+
try:
|
|
90
|
+
module = importlib.import_module(f"app.event.{module_name}")
|
|
91
|
+
for _, obj in inspect.getmembers(module, inspect.isclass):
|
|
92
|
+
if issubclass(obj, JDMEvent) and obj is not JDMEvent:
|
|
93
|
+
obj()
|
|
94
|
+
|
|
95
|
+
Printer.success(f"[event/{module_name}] loaded")
|
|
96
|
+
except Exception as e:
|
|
97
|
+
Printer.error(f"[event/{module_name}] failed to load — {e}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_app(config_name: str = None, static_folder: str = "static") -> Flask:
|
|
101
|
+
"""
|
|
102
|
+
Create and configure the Flask application.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
config_name: One of 'development', 'production', 'deployed'.
|
|
106
|
+
Defaults to FLASK_ENV env var, then 'development'.
|
|
107
|
+
static_folder: Path to the static folder (React build output).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Configured Flask app instance.
|
|
111
|
+
"""
|
|
112
|
+
app = Flask(__name__, static_folder=static_folder, static_url_path="/static")
|
|
113
|
+
CORS(app, origins="*")
|
|
114
|
+
|
|
115
|
+
if config_name is None:
|
|
116
|
+
config_name = os.getenv("FLASK_ENV", "development")
|
|
117
|
+
|
|
118
|
+
_load_config(config_name, app)
|
|
119
|
+
|
|
120
|
+
Printer.init(config_name)
|
|
121
|
+
Printer.info(f"Environment: {config_name}")
|
|
122
|
+
Printer.info(f"jdm-electron-flask-py v{_get_version()}")
|
|
123
|
+
|
|
124
|
+
# ── SocketIO ──────────────────────────────────────────────
|
|
125
|
+
_socketio.init_app(
|
|
126
|
+
app,
|
|
127
|
+
cors_allowed_origins="*",
|
|
128
|
+
logger=config_name == "development",
|
|
129
|
+
engineio_logger=config_name == "development",
|
|
130
|
+
)
|
|
131
|
+
# ─────────────────────────────────────────────────────────
|
|
132
|
+
Printer.log("\n Registering blueprints...")
|
|
133
|
+
_load_blueprints(app, config_name)
|
|
134
|
+
|
|
135
|
+
Printer.log("\n Loading socket events...")
|
|
136
|
+
_load_events()
|
|
137
|
+
|
|
138
|
+
# ── React SPA catch-all ───────────────────────────────────
|
|
139
|
+
@app.route("/", defaults={"path": ""})
|
|
140
|
+
@app.route("/<path:path>")
|
|
141
|
+
def serve_react(path):
|
|
142
|
+
if path and os.path.exists(os.path.join(app.static_folder, path)):
|
|
143
|
+
return send_from_directory(app.static_folder, path)
|
|
144
|
+
return send_from_directory(app.static_folder, "index.html")
|
|
145
|
+
# ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
return app
|
|
148
|
+
|
|
149
|
+
def _get_version() -> str:
|
|
150
|
+
try:
|
|
151
|
+
from jdm_electron_flask import __version__
|
|
152
|
+
return __version__
|
|
153
|
+
except Exception:
|
|
154
|
+
return "unknown"
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from flask import Blueprint, request
|
|
4
|
+
from jdm_electron_flask.utils.responses import success, error
|
|
5
|
+
from jdm_electron_flask.utils.validators import validate_json, require_access
|
|
6
|
+
from typing import Callable, List, Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class JDMBlueprint(Blueprint):
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: str, import_name: str, **kwargs):
|
|
12
|
+
super().__init__(name, import_name, **kwargs)
|
|
13
|
+
self.success = success
|
|
14
|
+
self.error = error
|
|
15
|
+
self.register_error_handler(404, self._handle_404)
|
|
16
|
+
self.register_error_handler(500, self._handle_500)
|
|
17
|
+
self._bind_all()
|
|
18
|
+
|
|
19
|
+
def _bind_all(self):
|
|
20
|
+
for _, func in inspect.getmembers(self.__class__, predicate=inspect.isfunction):
|
|
21
|
+
if hasattr(func, "_jdm_route"):
|
|
22
|
+
self._bind(func._jdm_route, func)
|
|
23
|
+
|
|
24
|
+
def _bind(self, definition: dict, func: Callable):
|
|
25
|
+
path = definition["path"]
|
|
26
|
+
methods = definition["methods"]
|
|
27
|
+
auth = definition.get("auth", False)
|
|
28
|
+
validate = definition.get("validate")
|
|
29
|
+
|
|
30
|
+
wrapped = func
|
|
31
|
+
if validate:
|
|
32
|
+
fields = validate if isinstance(validate, list) else [validate]
|
|
33
|
+
wrapped = validate_json(*fields)(wrapped)
|
|
34
|
+
if auth:
|
|
35
|
+
wrapped = require_access(wrapped)
|
|
36
|
+
|
|
37
|
+
if inspect.iscoroutinefunction(func) and not inspect.iscoroutinefunction(wrapped):
|
|
38
|
+
inner = wrapped
|
|
39
|
+
@functools.wraps(func)
|
|
40
|
+
async def async_wrapper(*args, **kwargs):
|
|
41
|
+
return await inner(*args, **kwargs)
|
|
42
|
+
wrapped = async_wrapper
|
|
43
|
+
|
|
44
|
+
self.add_url_rule(path, func.__name__, wrapped, methods=methods)
|
|
45
|
+
|
|
46
|
+
# ── Route decorators ──────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def get(path: str, auth: bool = False):
|
|
50
|
+
def decorator(func: Callable):
|
|
51
|
+
func._jdm_route = {"path": path, "methods": ["GET"], "auth": auth}
|
|
52
|
+
return func
|
|
53
|
+
return decorator
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def post(path: str, auth: bool = True, validate: Union[str, List[str], None] = None):
|
|
57
|
+
def decorator(func: Callable):
|
|
58
|
+
func._jdm_route = {"path": path, "methods": ["POST"], "auth": auth, "validate": validate}
|
|
59
|
+
return func
|
|
60
|
+
return decorator
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def put(path: str, auth: bool = True, validate: Union[str, List[str], None] = None):
|
|
64
|
+
def decorator(func: Callable):
|
|
65
|
+
func._jdm_route = {"path": path, "methods": ["PUT"], "auth": auth, "validate": validate}
|
|
66
|
+
return func
|
|
67
|
+
return decorator
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def delete(path: str, auth: bool = True):
|
|
71
|
+
def decorator(func: Callable):
|
|
72
|
+
func._jdm_route = {"path": path, "methods": ["DELETE"], "auth": auth}
|
|
73
|
+
return func
|
|
74
|
+
return decorator
|
|
75
|
+
|
|
76
|
+
def _handle_404(self, e):
|
|
77
|
+
return error(f"Route not found: {request.path}", status=404)
|
|
78
|
+
|
|
79
|
+
def _handle_500(self, e):
|
|
80
|
+
return error("Internal server error", status=500)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
|
|
5
|
+
def _load_env():
|
|
6
|
+
"""Load .env from PyInstaller bundle first, then CWD."""
|
|
7
|
+
meipass = getattr(sys, "_MEIPASS", None)
|
|
8
|
+
if meipass:
|
|
9
|
+
load_dotenv(os.path.join(meipass, ".env"))
|
|
10
|
+
load_dotenv(os.path.join(os.path.abspath("."), ".env"), override=False)
|
|
11
|
+
|
|
12
|
+
_load_env()
|
|
13
|
+
|
|
14
|
+
class JDMConfig:
|
|
15
|
+
"""Base config. Inherit this to add your own shared fields."""
|
|
16
|
+
SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class JDMDevelopmentConfig(JDMConfig):
|
|
20
|
+
FLASK_ENV = "development"
|
|
21
|
+
DEBUG = True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class JDMProductionConfig(JDMConfig):
|
|
25
|
+
FLASK_ENV = "production"
|
|
26
|
+
DEBUG = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class JDMDeployedConfig(JDMConfig):
|
|
30
|
+
FLASK_ENV = "deployed"
|
|
31
|
+
DEBUG = False
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# jdm_electron_flask/event.py
|
|
2
|
+
from flask_socketio import emit
|
|
3
|
+
from jdm_electron_flask import get_socketio
|
|
4
|
+
|
|
5
|
+
class JDMEvent:
|
|
6
|
+
"""
|
|
7
|
+
Base class for SocketIO event handlers.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from jdm_electron_flask import JDMEvent
|
|
11
|
+
|
|
12
|
+
class ConnectEvent(JDMEvent):
|
|
13
|
+
def on_connect(self):
|
|
14
|
+
self.emit("connected", {"message": "Socket connected"})
|
|
15
|
+
|
|
16
|
+
def on_disconnect(self):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def on_ping_server(self, data):
|
|
20
|
+
self.emit("pong_client", {"echo": data})
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._socketio = get_socketio()
|
|
25
|
+
self._register()
|
|
26
|
+
|
|
27
|
+
def emit(self, event: str, data=None, **kwargs):
|
|
28
|
+
"""Shorthand emit."""
|
|
29
|
+
emit(event, data, **kwargs)
|
|
30
|
+
|
|
31
|
+
def _register(self):
|
|
32
|
+
"""
|
|
33
|
+
Auto-register methods named on_<event> as SocketIO event handlers.
|
|
34
|
+
on_connect → "connect"
|
|
35
|
+
on_disconnect → "disconnect"
|
|
36
|
+
on_ping_server → "ping_server"
|
|
37
|
+
"""
|
|
38
|
+
for attr in dir(self):
|
|
39
|
+
if not attr.startswith("on_"):
|
|
40
|
+
continue
|
|
41
|
+
handler = getattr(self, attr)
|
|
42
|
+
if not callable(handler):
|
|
43
|
+
continue
|
|
44
|
+
event_name = attr[3:]
|
|
45
|
+
self._socketio.on_event(event_name, handler)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Printer:
|
|
7
|
+
"""
|
|
8
|
+
Environment-aware logger.
|
|
9
|
+
|
|
10
|
+
- development: prints to console with icons
|
|
11
|
+
- production / deployed: writes to logs/<date>.log
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
_logger = None
|
|
15
|
+
_env: str = None
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def init(cls, env: str):
|
|
19
|
+
cls._env = env
|
|
20
|
+
|
|
21
|
+
if env != "development":
|
|
22
|
+
log_dir = os.path.join(os.getcwd(), "logs")
|
|
23
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
24
|
+
|
|
25
|
+
log_filename = datetime.now().strftime("%Y-%m-%d") + ".log"
|
|
26
|
+
log_path = os.path.join(log_dir, log_filename)
|
|
27
|
+
|
|
28
|
+
logging.basicConfig(
|
|
29
|
+
filename=log_path,
|
|
30
|
+
level=logging.INFO,
|
|
31
|
+
format="%(asctime)s [%(levelname)s] %(message)s",
|
|
32
|
+
datefmt="%H:%M:%S",
|
|
33
|
+
)
|
|
34
|
+
cls._logger = logging.getLogger("jdm")
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def log(cls, message: str):
|
|
38
|
+
if cls._env == "development":
|
|
39
|
+
print(message)
|
|
40
|
+
else:
|
|
41
|
+
cls._logger.info(message)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def success(cls, message: str):
|
|
45
|
+
if cls._env == "development":
|
|
46
|
+
print(f" ✔ {message}")
|
|
47
|
+
else:
|
|
48
|
+
cls._logger.info(f"[SUCCESS] {message}")
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def warn(cls, message: str):
|
|
52
|
+
if cls._env == "development":
|
|
53
|
+
print(f" ⚠ {message}")
|
|
54
|
+
else:
|
|
55
|
+
cls._logger.warning(f"[WARN] {message}")
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def error(cls, message: str):
|
|
59
|
+
if cls._env == "development":
|
|
60
|
+
print(f" ✖ {message}")
|
|
61
|
+
else:
|
|
62
|
+
cls._logger.error(f"[ERROR] {message}")
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def info(cls, message: str):
|
|
66
|
+
if cls._env == "development":
|
|
67
|
+
print(f" ℹ {message}")
|
|
68
|
+
else:
|
|
69
|
+
cls._logger.info(f"[INFO] {message}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from flask import jsonify
|
|
2
|
+
|
|
3
|
+
def success(data=None, message="OK", status=200):
|
|
4
|
+
"""Unified success response."""
|
|
5
|
+
resp = {"success": True, "message": message}
|
|
6
|
+
if data is not None:
|
|
7
|
+
resp["data"] = data
|
|
8
|
+
return jsonify(resp), status
|
|
9
|
+
|
|
10
|
+
def error(message, status=400, details=None):
|
|
11
|
+
"""Unified error response."""
|
|
12
|
+
resp = {"success": False, "message": message}
|
|
13
|
+
if details:
|
|
14
|
+
resp["details"] = details
|
|
15
|
+
return jsonify(resp), status
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# import os
|
|
2
|
+
# from functools import wraps
|
|
3
|
+
# from flask import request
|
|
4
|
+
# from .responses import error
|
|
5
|
+
|
|
6
|
+
# def validate_json(*required_fields):
|
|
7
|
+
# """Decorator: ensure request has JSON and required fields."""
|
|
8
|
+
# def decorator(f):
|
|
9
|
+
# @wraps(f)
|
|
10
|
+
# def wrapper(*args, **kwargs):
|
|
11
|
+
# if not request.is_json:
|
|
12
|
+
# return error("Request must be application/json", 415)
|
|
13
|
+
# data = request.get_json()
|
|
14
|
+
# if data is None:
|
|
15
|
+
# return error("Invalid or empty JSON body", 400)
|
|
16
|
+
# missing = [field for field in required_fields if field not in data]
|
|
17
|
+
# if missing:
|
|
18
|
+
# return error(f"Missing fields: {', '.join(missing)}", 400)
|
|
19
|
+
# return f(data, *args, **kwargs)
|
|
20
|
+
# return wrapper
|
|
21
|
+
# return decorator
|
|
22
|
+
|
|
23
|
+
# def require_access(f):
|
|
24
|
+
# """Decorator: validates X-Auth-Token header against API_ACCESS in .env."""
|
|
25
|
+
# @wraps(f)
|
|
26
|
+
# def wrapper(*args, **kwargs):
|
|
27
|
+
# auth_header = request.headers.get("X-Auth-Token", "")
|
|
28
|
+
# if not auth_header.startswith("Bearer "):
|
|
29
|
+
# return error("Missing or invalid X-Auth-Token header", 401)
|
|
30
|
+
|
|
31
|
+
# token = auth_header.split(" ", 1)[1].strip()
|
|
32
|
+
# expected = os.getenv("API_ACCESS", "")
|
|
33
|
+
|
|
34
|
+
# if not expected:
|
|
35
|
+
# return error("Server is not configured with an API access token", 500)
|
|
36
|
+
# if token != expected:
|
|
37
|
+
# return error("Unauthorized", 401)
|
|
38
|
+
|
|
39
|
+
# return f(*args, **kwargs)
|
|
40
|
+
# return wrapper
|
|
41
|
+
|
|
42
|
+
import os
|
|
43
|
+
import inspect
|
|
44
|
+
from functools import wraps
|
|
45
|
+
from flask import request
|
|
46
|
+
from .responses import error
|
|
47
|
+
|
|
48
|
+
def validate_json(*required_fields):
|
|
49
|
+
"""Decorator: ensure request has JSON and required fields."""
|
|
50
|
+
def decorator(f):
|
|
51
|
+
if inspect.iscoroutinefunction(f):
|
|
52
|
+
@wraps(f)
|
|
53
|
+
async def wrapper(*args, **kwargs):
|
|
54
|
+
if not request.is_json:
|
|
55
|
+
return error("Request must be application/json", 415)
|
|
56
|
+
data = request.get_json()
|
|
57
|
+
if data is None:
|
|
58
|
+
return error("Invalid or empty JSON body", 400)
|
|
59
|
+
missing = [field for field in required_fields if field not in data]
|
|
60
|
+
if missing:
|
|
61
|
+
return error(f"Missing fields: {', '.join(missing)}", 400)
|
|
62
|
+
return await f(data, *args, **kwargs)
|
|
63
|
+
else:
|
|
64
|
+
@wraps(f)
|
|
65
|
+
def wrapper(*args, **kwargs):
|
|
66
|
+
if not request.is_json:
|
|
67
|
+
return error("Request must be application/json", 415)
|
|
68
|
+
data = request.get_json()
|
|
69
|
+
if data is None:
|
|
70
|
+
return error("Invalid or empty JSON body", 400)
|
|
71
|
+
missing = [field for field in required_fields if field not in data]
|
|
72
|
+
if missing:
|
|
73
|
+
return error(f"Missing fields: {', '.join(missing)}", 400)
|
|
74
|
+
return f(data, *args, **kwargs)
|
|
75
|
+
return wrapper
|
|
76
|
+
return decorator
|
|
77
|
+
|
|
78
|
+
def require_access(f):
|
|
79
|
+
"""Decorator: validates X-Auth-Token header against API_ACCESS in .env."""
|
|
80
|
+
if inspect.iscoroutinefunction(f):
|
|
81
|
+
@wraps(f)
|
|
82
|
+
async def wrapper(*args, **kwargs):
|
|
83
|
+
auth_header = request.headers.get("X-Auth-Token", "")
|
|
84
|
+
if not auth_header.startswith("Bearer "):
|
|
85
|
+
return error("Missing or invalid X-Auth-Token header", 401)
|
|
86
|
+
token = auth_header.split(" ", 1)[1].strip()
|
|
87
|
+
expected = os.getenv("API_ACCESS", "")
|
|
88
|
+
if not expected:
|
|
89
|
+
return error("Server is not configured with an API access token", 500)
|
|
90
|
+
if token != expected:
|
|
91
|
+
return error("Unauthorized", 401)
|
|
92
|
+
return await f(*args, **kwargs)
|
|
93
|
+
else:
|
|
94
|
+
@wraps(f)
|
|
95
|
+
def wrapper(*args, **kwargs):
|
|
96
|
+
auth_header = request.headers.get("X-Auth-Token", "")
|
|
97
|
+
if not auth_header.startswith("Bearer "):
|
|
98
|
+
return error("Missing or invalid X-Auth-Token header", 401)
|
|
99
|
+
token = auth_header.split(" ", 1)[1].strip()
|
|
100
|
+
expected = os.getenv("API_ACCESS", "")
|
|
101
|
+
if not expected:
|
|
102
|
+
return error("Server is not configured with an API access token", 500)
|
|
103
|
+
if token != expected:
|
|
104
|
+
return error("Unauthorized", 401)
|
|
105
|
+
return f(*args, **kwargs)
|
|
106
|
+
return wrapper
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jdm-electron-flask
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The Python backbone for jdm-electron-flask desktop apps
|
|
5
|
+
Author-email: JDM-Github <jdmaster888@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JDM-Github/jdm-electron-flask
|
|
8
|
+
Project-URL: Repository, https://github.com/JDM-Github/jdm-electron-flask
|
|
9
|
+
Project-URL: Issues, https://github.com/JDM-Github/jdm-electron-flask/issues
|
|
10
|
+
Keywords: flask,electron,desktop,socketio,jdm
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: flask>=3.0.0
|
|
23
|
+
Requires-Dist: flask-cors>=4.0.0
|
|
24
|
+
Requires-Dist: flask-socketio>=5.3.0
|
|
25
|
+
Requires-Dist: simple-websocket>=1.0.0
|
|
26
|
+
|
|
27
|
+
# jdm-electron-flask (Python)
|
|
28
|
+
|
|
29
|
+
> The Python backbone for [`jdm-electron-flask`](https://github.com/JDM-Github/jdm-electron-flask) desktop apps.
|
|
30
|
+
|
|
31
|
+
This package ships inside every project scaffolded by the `jdm-cli electron-flask` plugin. It provides the app factory, dynamic API registration, SocketIO setup, and environment-aware logging — so you build features, not boilerplate.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
You don't install this manually. It's included in the scaffolded `requirements.txt`:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
jdm-electron-flask==1.0.0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If you need it standalone:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install jdm-electron-flask
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## What it provides
|
|
52
|
+
|
|
53
|
+
### `create_app`
|
|
54
|
+
The Flask app factory. Handles config loading, SocketIO init, blueprint auto-discovery, and the React SPA catch-all route.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# run.py
|
|
58
|
+
from jdm_electron_flask import create_app, get_socketio
|
|
59
|
+
|
|
60
|
+
app = create_app()
|
|
61
|
+
socketio = get_socketio()
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
socketio.run(app, host="0.0.0.0", port=5000, debug=True)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `JDMBlueprint`
|
|
68
|
+
Extended Blueprint with built-in response helpers.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from jdm_electron_flask import JDMBlueprint
|
|
72
|
+
|
|
73
|
+
health_bp = JDMBlueprint("health", __name__)
|
|
74
|
+
|
|
75
|
+
@health_bp.route("/health")
|
|
76
|
+
def health():
|
|
77
|
+
return health_bp.success({"status": "ok"})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### `JDMEvents`
|
|
81
|
+
Base class for organizing socket event handlers.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from jdm_electron_flask import JDMEvents
|
|
85
|
+
|
|
86
|
+
class ConnectEvents(JDMEvents):
|
|
87
|
+
def register(self):
|
|
88
|
+
@self.socketio.on("connect")
|
|
89
|
+
def on_connect():
|
|
90
|
+
self.on_connect()
|
|
91
|
+
|
|
92
|
+
@self.socketio.on("ping_server")
|
|
93
|
+
def on_ping(data):
|
|
94
|
+
self.emit("pong_client", {"echo": data})
|
|
95
|
+
|
|
96
|
+
ConnectEvents().register()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `Printer`
|
|
100
|
+
Environment-aware logger. Prints in development, writes to `logs/<date>.log` in production/deployed.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from jdm_electron_flask import Printer
|
|
104
|
+
|
|
105
|
+
Printer.success("Blueprint registered")
|
|
106
|
+
Printer.warn("Something is off")
|
|
107
|
+
Printer.error("Something broke")
|
|
108
|
+
Printer.info("Just FYI")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Response helpers
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from jdm_electron_flask import success, error, paginate
|
|
115
|
+
|
|
116
|
+
return success({"user": "JDM"})
|
|
117
|
+
return error("Not found", status=404)
|
|
118
|
+
return paginate(items, total=100, page=1, per_page=10)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Validators
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from jdm_electron_flask import validate_fields
|
|
125
|
+
|
|
126
|
+
valid, msg, data = validate_fields(["name", "email"])
|
|
127
|
+
if not valid:
|
|
128
|
+
return error(msg)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Dynamic API registration
|
|
134
|
+
|
|
135
|
+
Routes are controlled by `config/api.json`:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"health": {
|
|
140
|
+
"link": "/api",
|
|
141
|
+
"masterEnabled": true
|
|
142
|
+
},
|
|
143
|
+
"users": {
|
|
144
|
+
"link": "/api/users",
|
|
145
|
+
"masterEnabled": true,
|
|
146
|
+
"disabledOnProduction": false,
|
|
147
|
+
"disabledOnDeployed": false
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Adding a new route:
|
|
153
|
+
1. Add entry to `config/api.json`
|
|
154
|
+
2. Create `app/api/users.py` with a `users_bp` blueprint
|
|
155
|
+
|
|
156
|
+
That's it. No touching `__init__.py` ever again.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Environment modes
|
|
161
|
+
|
|
162
|
+
| `FLASK_ENV` | Behavior |
|
|
163
|
+
|---|---|
|
|
164
|
+
| `development` | Debug on, console logging, all routes enabled |
|
|
165
|
+
| `production` | Debug off, file logging, respects `disabledOnProduction` |
|
|
166
|
+
| `deployed` | Debug off, file logging, respects `disabledOnDeployed` |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Companion
|
|
171
|
+
|
|
172
|
+
This package is the Python half of the `jdm-electron-flask` ecosystem:
|
|
173
|
+
|
|
174
|
+
| Package | Role |
|
|
175
|
+
|---|---|
|
|
176
|
+
| [`jdm-electron-flask`](https://www.npmjs.com/package/jdm-electron-flask) (npm) | CLI plugin — scaffold, build, compile |
|
|
177
|
+
| `jdm-electron-flask` (PyPI) | Backend library — app factory, blueprints, sockets |
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT © [JDM-Github](https://github.com/JDM-Github)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
jdm_electron_flask/__init__.py
|
|
4
|
+
jdm_electron_flask.egg-info/PKG-INFO
|
|
5
|
+
jdm_electron_flask.egg-info/SOURCES.txt
|
|
6
|
+
jdm_electron_flask.egg-info/dependency_links.txt
|
|
7
|
+
jdm_electron_flask.egg-info/requires.txt
|
|
8
|
+
jdm_electron_flask.egg-info/top_level.txt
|
|
9
|
+
jdm_electron_flask/core/app.py
|
|
10
|
+
jdm_electron_flask/core/blueprint.py
|
|
11
|
+
jdm_electron_flask/core/config.py
|
|
12
|
+
jdm_electron_flask/core/event.py
|
|
13
|
+
jdm_electron_flask/utils/printer.py
|
|
14
|
+
jdm_electron_flask/utils/responses.py
|
|
15
|
+
jdm_electron_flask/utils/validators.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jdm_electron_flask
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jdm-electron-flask"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "The Python backbone for jdm-electron-flask desktop apps"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "JDM-Github", email = "jdmaster888@gmail.com" }]
|
|
12
|
+
keywords = ["flask", "electron", "desktop", "socketio", "jdm"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.9"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"flask>=3.0.0",
|
|
27
|
+
"flask-cors>=4.0.0",
|
|
28
|
+
"flask-socketio>=5.3.0",
|
|
29
|
+
"simple-websocket>=1.0.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/JDM-Github/jdm-electron-flask"
|
|
34
|
+
Repository = "https://github.com/JDM-Github/jdm-electron-flask"
|
|
35
|
+
Issues = "https://github.com/JDM-Github/jdm-electron-flask/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["jdm_electron_flask*"]
|