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.
@@ -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,4 @@
1
+ flask>=3.0.0
2
+ flask-cors>=4.0.0
3
+ flask-socketio>=5.3.0
4
+ simple-websocket>=1.0.0
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+