Laravel-modules inspired modular architecture for Python — framework agnostic.
Project description
pymodules
Modular application architecture for Python — inspired by Laravel Modules, built for the Python ecosystem.
pymodules lets you split large Python applications into self-contained, independently toggleable modules — each with its own models, views, routes, config, migrations, service providers, and tests. It works with any Python project and ships with first-class support for Django, Flask, and FastAPI.
Table of Contents
- Why pymodules?
- Version 0.1.0
- V2 Migration Guide (Preview)
- Installation
- Quick Start
- Module Structure
- The module.json Manifest
- CLI Reference — pymodules
- Framework Detection
- Scaffold Presets
- Service Providers
- Django Integration
- Flask Integration
- FastAPI Integration
- Pure Python Usage
- Project Configuration — pymodules.toml
- API Reference
- Feature Status
- Release Process
- Roadmap to Industry Standard
- Contributing
- License
Why pymodules?
A typical Django or FastAPI project starts flat and becomes a maze:
myproject/
models.py # 800 lines, everything mixed together
views.py # 1200 lines
urls.py # references all 40 views
settings.py # references everything
pymodules enforces a boundary-first architecture. Every feature is a module:
myproject/
modules/
Blog/ # completely self-contained
Shop/ # independent — can be disabled with one line
Auth/ # has its own models, routes, config, migrations
Notifications/
Each module is a Python package that can be enabled or disabled at runtime by flipping a flag in its module.json. Disabled modules are not imported, not booted, and not registered with your framework.
This is the Python equivalent of nWidart/laravel-modules — the most popular Laravel package for large application architecture.
Version 0.1.0
This is the first public release of pymodules. It is production-usable for greenfield projects and stable enough for evaluation in existing projects.
What 0.1.0 delivers:
- Framework-agnostic core (
ModuleRegistry,Module,ServiceProvider) - Auto-detection of Django, FastAPI, Flask from your environment
pymodulesCLI withinit,make,list,enable,disable,show,delete,publish,detect,presets- Django
manage.pyintegration (module_make,module_list,module_enable,module_disable,module_show,module_delete,module_publish) - Eight scaffold presets:
plain,default,django,django-api,fastapi,fastapi-crud,flask,flask-api - Per-project config via
pymodules.toml - Custom folder naming — call it
modules/,plugins/,apps/,src/features/— anything - Module manifest (
module.json) with versioning, enable/disable, provider registration - Service Provider pattern for boot-time registration logic
- Module-level settings merged into Django settings
- Module-level URL routing for all three frameworks
- Module-level migrations for Django
- Asset publishing system
V2 Migration Guide (Preview)
v2 features are being added in a backward-compatible way.
If your project already uses service providers, you can keep using it exactly as-is. You only need to change things when you want to adopt new v2 features.
Quick summary:
- no immediate rewrite required
- current module.json + providers setup still works
- typed modules and entry points are optional
1. v1 compatibility remains enabled
Current provider-based modules continue to work without changes:
- modules are discovered from your modules folder
- module.json providers are loaded
- providers still run in register then boot order
2. Typed modules are opt-in
When you are ready, migrate one module at a time by adding module_class to module.json:
{
"name": "Billing",
"enabled": true,
"module_class": "modules.Billing.runtime.BillingModule",
"providers": []
}
That class should inherit from BaseModule and can register extension data through the registry.
3. Entry-point discovery is opt-in
By default, entry-point scanning is off so existing projects do not change behavior.
from pymodules import ModuleRegistry
registry = ModuleRegistry(
modules_path="modules",
include_entry_points=True,
)
4. Discovery precedence is configurable
If both filesystem modules and entry-point modules are enabled, you can control precedence and duplicate handling:
registry = ModuleRegistry(
modules_path="modules",
include_entry_points=True,
discovery_order=("filesystem", "entry_points"),
duplicate_policy="error", # "error" | "prefer-first" | "prefer-last"
)
Recommended migration path:
- Start with defaults (include_entry_points=False).
- Migrate one module to module_class and validate behavior.
- Enable entry points only when packaging modules externally.
- Keep duplicate_policy="error" during migration so collisions fail fast.
Common question: will this break my existing modules?
- Not by default.
- Breakage usually happens only if you opt into new discovery modes and have duplicate module keys.
- Keeping duplicate_policy="error" prevents silent overrides.
For architecture details, see docs/v2_spec.md.
Installation
# Core — no framework dependencies (pure Python, FastAPI, Flask)
pip install pytmodules
# With Django support
pip install "pytmodules[django]"
# With Django + Django REST Framework scaffolding support
pip install "pytmodules[django-api]"
# With Flask support
pip install "pytmodules[flask]"
# With FastAPI support
pip install "pytmodules[fastapi]"
# Everything
pip install "pytmodules[django,flask,fastapi]"
From GitHub (development installs):
pip install git+https://github.com/tomcroot/pymodules.git
pip install "pytmodules[django] @ git+https://github.com/tomcroot/pymodules.git"
Requirements: Python 3.10+
Release Process
Releases are automated with GitHub Actions:
- .github/workflows/release.yml (manual trigger) bumps version files, creates/pushes a
v*tag, and creates the GitHub Release. - .github/workflows/publish.yml runs on tag push (
v*), builds artifacts, runstwine check, and publishes to PyPI via trusted publishing.
Local validation before cutting a release:
python -m build
python -m twine check dist/*
Release flow:
- Ensure
CHANGELOG.mdhas release-ready notes under[Unreleased]. - Run the Release workflow from GitHub Actions (
workflow_dispatch) and provide the version (for example,0.1.1). - The workflow updates version metadata, commits, tags (
v0.1.1), and creates the GitHub Release. - Tag push automatically triggers the publish workflow, which builds and uploads to PyPI.
The publish workflow is configured for PyPI trusted publishing. Keep the GitHub pypi environment and PyPI trusted publisher settings in sync if repository, workflow, or environment names change.
Quick Start
Any Python project
cd myproject
pymodules init # auto-detects your framework, creates pymodules.toml
pymodules make Blog # scaffold a Blog module using detected framework
Django project
pip install "pytmodules[django]"
pymodules init # detects Django, writes pymodules.toml
pymodules make Blog # creates full Django module scaffold
# Or with manage.py (once "pymodules" is in INSTALLED_APPS):
python manage.py module_make Blog
FastAPI project
pip install "pytmodules[fastapi]"
pymodules init
pymodules make Blog # creates FastAPI module with APIRouter + Pydantic schemas
Flask project
pip install "pytmodules[flask]"
pymodules init
pymodules make Blog # creates Flask module with Blueprint
Module Structure
Every module is a Python package directory inside your modules folder. The layout depends on the scaffold preset used, but the full structure (from the django preset) is:
modules/
└── Blog/
├── module.json ← required: manifest, version, enabled flag, providers
├── __init__.py
├── apps.py ← Django AppConfig (django preset)
├── providers.py ← ServiceProvider subclass
├── routes.py ← URL patterns / Blueprint / APIRouter
├── admin.py ← Django admin registration (django preset)
├── serializers.py ← DRF serializers, commented (django preset)
├── schemas.py ← Pydantic schemas (fastapi preset)
├── services.py ← Business logic layer (fastapi/flask presets)
├── models/
│ ├── __init__.py
│ └── blog.py ← Django model (django preset)
├── views/
│ ├── __init__.py
│ └── blog_views.py ← Django views (django preset)
├── config/
│ ├── __init__.py
│ └── config.py ← Module-level settings (all presets)
├── database/
│ └── migrations/ ← Django migrations (django preset)
│ └── __init__.py
├── assets/ ← Publishable static files
└── tests/
├── __init__.py
└── test_blog.py ← Starter test class
The module.json Manifest
Every module has a module.json at its root. This is the single source of truth for the module's identity and state.
{
"name": "Blog",
"version": "1.0.0",
"description": "Blog module with posts, tags and comments.",
"author": "Your Name",
"enabled": true,
"requires": ["Core"],
"providers": [
"modules.Blog.providers.BlogServiceProvider"
],
"publishes": {
"default": {
"assets/blog.css": "static/blog/blog.css"
},
"config": {
"config/config.py": "config/blog_config.py"
}
}
}
| Field | Required | Description |
|---|---|---|
name |
Yes | Module name (PascalCase) |
version |
No | Semantic version string |
description |
No | Human-readable description |
author |
No | Author name or email |
enabled |
Yes | true/false — controls whether the module boots |
requires |
No | List of module names that must boot before this module |
providers |
No | Dotted paths to ServiceProvider subclasses |
publishes |
No | Asset publish map (group → source → destination) |
CLI Reference — pymodules
The pymodules CLI is available after installation. All commands read pymodules.toml for defaults.
pymodules init
Initialise pymodules in a project. Auto-detects your framework and writes pymodules.toml.
pymodules init # fully automatic
pymodules init --path plugins # custom folder name
pymodules init --path apps --preset django # explicit preset
pymodules init --force # overwrite existing config
pymodules detect
Show which framework was detected and why.
pymodules detect
pymodules detect --path /other/project
Output:
Framework : django
Preset : django
Confidence : high
Reason : django is installed in the active Python environment;
found django-specific project files (manage.py)
pymodules make
Create a new module scaffold.
pymodules make Blog # auto-detected preset
pymodules make Blog --preset django # explicit preset
pymodules make Blog --preset fastapi
pymodules make Blog --preset flask
pymodules make Blog --preset plain # bare minimum
pymodules make Blog --force # overwrite existing
pymodules list
List all modules and their status.
pymodules list
pymodules list --enabled-only
pymodules list --disabled-only
pymodules enable / pymodules disable
Toggle a module on or off. Writes to module.json — persists across restarts.
pymodules enable Blog
pymodules disable Blog
pymodules show
Show full details for a single module.
pymodules show Blog
pymodules delete
Delete a module from disk.
pymodules delete Blog
pymodules delete Blog --yes # skip confirmation
pymodules publish
Copy a module's publishable assets to the host application.
pymodules publish Blog
pymodules publish Blog --group config
pymodules publish # publish all enabled modules
pymodules publish --force # overwrite existing files
pymodules presets
List all available scaffold presets.
pymodules presets
Global options
pymodules --modules-path src/apps make Blog # override modules folder
Set PYMODULES_PATH environment variable to avoid passing --modules-path on every command.
Framework Detection
pymodules detects your framework automatically using a three-layer strategy:
| Layer | Method | Confidence boost |
|---|---|---|
| 1 | importlib.util.find_spec() — is the package installed in this venv? |
+10 |
| 1b | DJANGO_SETTINGS_MODULE environment variable set? |
+5 (Django only) |
| 2 | Project file fingerprints (manage.py, wsgi.py, app.py …) |
+3 |
| 3 | Dependency file scanning (requirements.txt, pyproject.toml, Pipfile, setup.cfg) |
+2 |
When multiple frameworks are detected, priority is Django > FastAPI > Flask.
Use the detector in your own code:
from pymodules import detect_framework
info = detect_framework()
print(info.name) # "django" | "fastapi" | "flask" | "unknown"
print(info.confidence) # "high" | "medium" | "low"
print(info.preset) # matching preset name
print(info.reason) # human-readable explanation
Scaffold Presets
Presets control exactly which files are generated by pymodules make.
plain
Bare minimum. Use when you want to build your own structure.
Blog/
module.json
__init__.py
default
Framework-agnostic. A clean starting point for any Python project.
Blog/
module.json, __init__.py, providers.py
config/config.py
tests/test_blog.py
assets/
django
Full Django module. Everything you need to add a new bounded context.
Blog/
module.json, __init__.py, apps.py, providers.py
models/blog.py ← ready-to-use Django model
views/blog_views.py ← index + detail views
routes.py ← urlpatterns + prefix
admin.py ← commented ModelAdmin
serializers.py ← commented DRF serializer
config/config.py
database/migrations/__init__.py
tests/test_blog.py
assets/
django-api
Django REST module with DRF serializer, viewset, and router wiring.
Blog/
module.json, __init__.py, apps.py, providers.py
models/blog.py
serializers.py ← DRF ModelSerializer + ListSerializer
policies.py ← DRF AccessPolicy scaffold for API permissions
viewsets.py ← DRF ModelViewSet
api/urls.py ← DefaultRouter wiring
routes.py ← thin wrapper delegating to api/urls.py
admin.py
config/config.py
database/migrations/__init__.py
tests/test_blog.py
assets/
fastapi
FastAPI module with 3-endpoint API skeleton.
Blog/
module.json, __init__.py, providers.py
routes.py ← APIRouter with list/get/create endpoints
schemas.py ← Pydantic BaseModel (Schema + CreateSchema)
services.py ← Service class (business logic layer)
config/config.py
tests/test_blog.py
fastapi-crud
FastAPI module with full CRUD endpoints.
Blog/
module.json, __init__.py, providers.py
routes.py ← APIRouter with list/get/create/update/delete endpoints
schemas.py ← Schema + CreateSchema + UpdateSchema
services.py ← includes update() and delete() skeletons
config/config.py
tests/test_blog.py
flask
Flask module with Blueprint.
Blog/
module.json, __init__.py, providers.py
routes.py ← Blueprint with index + detail routes
services.py ← Service class
config/config.py
tests/test_blog.py
flask-api
Flask module with JSON REST CRUD endpoints.
Blog/
module.json, __init__.py, providers.py
routes.py ← /api/{module}/ CRUD Blueprint
services.py ← all/create/find/update/delete skeletons
config/config.py
tests/test_blog.py
Service Providers
Service providers run arbitrary boot logic when a module is loaded. They are the correct place to register signals, middleware, event listeners, DI bindings, or any other setup that needs to run once at startup.
# modules/Blog/providers.py
from pymodules import ServiceProvider
class BlogServiceProvider(ServiceProvider):
def register(self) -> None:
"""
Called during module boot.
Register bindings, connect signals, set up DI.
"""
from django.db.models.signals import post_save
from .models.blog import Post
from .listeners import on_post_saved
post_save.connect(on_post_saved, sender=Post)
def boot(self) -> None:
"""
Called after all modules have registered.
Use for setup that depends on other modules.
"""
pass
Reference providers in module.json:
{
"providers": [
"modules.Blog.providers.BlogServiceProvider"
]
}
Django Integration
settings.py
from pathlib import Path
from pymodules.integrations.django import DjangoModuleRegistry
BASE_DIR = Path(__file__).resolve().parent.parent
MODULE_REGISTRY = DjangoModuleRegistry(modules_path=BASE_DIR / "modules")
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
# ...
"pymodules", # registers manage.py module_* commands
*MODULE_REGISTRY.installed_apps(), # auto-registers all enabled modules
]
# Point Django to each module's own migrations folder
MIGRATION_MODULES = MODULE_REGISTRY.migration_modules()
# Merge UPPER_CASE variables from every module's config/config.py
locals().update(MODULE_REGISTRY.collect_settings())
urls.py
from django.contrib import admin
from django.urls import path
from django.conf import settings
urlpatterns = [
path("admin/", admin.site.urls),
*settings.MODULE_REGISTRY.url_patterns(), # auto-includes all module routes
*settings.MODULE_REGISTRY.api_url_patterns(), # auto-includes /api/<module>/ routes
]
Module routes.py
# modules/Blog/routes.py
from django.urls import path
from .views.blog_views import index, detail
prefix = "blog" # mounted at /blog/
app_name = "blog"
urlpatterns = [
path("", index, name="index"),
path("<int:pk>/", detail, name="detail"),
]
Module-level settings
# modules/Blog/config/config.py
BLOG_POSTS_PER_PAGE = 10
BLOG_ALLOW_COMMENTS = True
BLOG_CACHE_TTL = 300
These are automatically merged into Django settings via collect_settings().
API permissions with collect_policies()
For DRF-based modules, DjangoModuleRegistry.collect_policies() auto-discovers
AccessPolicy subclasses from enabled modules. This is library-level plumbing:
pymodules handles discovery and naming conventions, while each application
keeps its actual permission rules inside its own modules.
collect_policies() supports either:
- a flat
policies.pyfile for simple modules - a
policies/package for larger modules
Example settings.py usage:
from pathlib import Path
from pymodules.integrations.django import DjangoModuleRegistry
BASE_DIR = Path(__file__).resolve().parent.parent
MODULE_REGISTRY = DjangoModuleRegistry(modules_path=BASE_DIR / "modules")
MODULE_POLICIES = MODULE_REGISTRY.collect_policies()
Example django-api scaffold output:
# modules/Blog/policies.py
from rest_framework_access_policy import AccessPolicy
class BlogPolicy(AccessPolicy):
statements = [
{
"action": ["list", "retrieve"],
"principal": "authenticated",
"effect": "allow",
},
]
Direct binding inside a DRF viewset:
from rest_framework import viewsets
from .models.blog import Blog
from .policies import BlogPolicy
from .serializers import BlogSerializer
class BlogViewSet(viewsets.ModelViewSet):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
permission_classes = [BlogPolicy]
Registry lookup when you want indirection:
from django.conf import settings
BlogPolicy = settings.MODULE_REGISTRY.get_policy("modules.Blog.policies.BlogPolicy")
If you use a policies/ package, keys reflect the real import path, for example
modules.HR.policies.employee.EmployeePolicy.
This feature requires drf-access-policy, which is included in
pytmodules[django-api].
manage.py commands
Once "pymodules" is in INSTALLED_APPS, all module_* commands are available:
python manage.py module_make Blog
python manage.py module_make Blog --preset django
python manage.py module_make Blog --force
python manage.py module_list
python manage.py module_list --enabled
python manage.py module_list --disabled
python manage.py module_show Blog
python manage.py module_enable Blog
python manage.py module_disable Blog
python manage.py module_delete Blog
python manage.py module_delete Blog --yes
python manage.py module_publish Blog
python manage.py module_publish Blog --group config
python manage.py module_publish --force
python manage.py module_make_model Blog Post
python manage.py module_make_model Blog ArchivedPost --proxy --parent Post
python manage.py module_make_serializer Blog Post
python manage.py module_make_viewset Blog Post
python manage.py module_make_api_urls Blog
python manage.py module_make_model_migration Blog Post --auto-name
python manage.py module_make_migration Blog
python manage.py module_migrate Blog
module_make without --preset defaults to "django" when running via manage.py — because the fact that you're using manage.py is itself proof this is a Django project.
Flask Integration
from flask import Flask
from pymodules.integrations.flask import FlaskModuleRegistry
app = Flask(__name__)
registry = FlaskModuleRegistry(modules_path="modules", app=app)
registry.boot() # auto-registers all enabled module Blueprints
App factory pattern:
registry = FlaskModuleRegistry(modules_path="modules")
def create_app():
app = Flask(__name__)
registry.init_app(app) # boots and registers Blueprints
return app
Module routes.py:
# modules/Blog/routes.py
from flask import Blueprint, jsonify
blueprint = Blueprint("blog", __name__, url_prefix="/blog")
@blueprint.get("/")
def index():
return jsonify([])
FastAPI Integration
from fastapi import FastAPI
from pymodules.integrations.fastapi import FastAPIModuleRegistry
app = FastAPI()
registry = FastAPIModuleRegistry(modules_path="modules", app=app)
registry.boot() # auto-includes all enabled module APIRouters
App factory pattern:
registry = FastAPIModuleRegistry(modules_path="modules")
def create_app():
app = FastAPI()
registry.init_app(app)
return app
Module routes.py:
# modules/Blog/routes.py
from fastapi import APIRouter
router = APIRouter(prefix="/blog", tags=["blog"])
@router.get("/")
def index():
return {"message": "Blog index"}
Pure Python Usage
No framework required. Use ModuleRegistry directly:
from pymodules import ModuleRegistry
registry = ModuleRegistry(modules_path="modules")
registry.boot()
# Find a module
blog = registry.find("Blog")
print(blog.name, blog.version, blog.is_enabled)
# Enable / disable (writes to module.json)
registry.enable("Shop")
registry.disable("Blog")
# Iterate
for module in registry:
print(module)
# Check existence
if "Blog" in registry:
print("Blog module is registered")
# Boot hooks — run for every enabled module at boot time
@registry.on_boot
def on_module_boot(module):
print(f"Booted: {module.name}")
# Import a submodule
models = blog.import_submodule("models.post")
Project Configuration — pymodules.toml
pymodules.toml lives at your project root and is committed to version control. It means you and your team never have to pass --modules-path manually.
# pymodules.toml
[pymodules]
# Where your modules/plugins live.
# Can be any name: "modules", "plugins", "apps", "src/features", etc.
modules_path = "modules"
# Default scaffold preset for `pymodules make` and `manage.py module_make`.
# Auto-detected from your environment — change if detection is wrong.
# Choices: default | plain | django | fastapi | flask
default_preset = "django"
# Informational — set by `pymodules init`, not read by the tool.
detected_framework = "django"
Config resolution order (highest priority first):
--modules-path/--presetCLI flagspymodules.tomlvalues- Auto-detected framework
- Hardcoded fallbacks (
"modules"/"default")
API Reference
Terminology used in this section:
- CLI commands: terminal commands such as
pymodules make Blog. - Python methods: code calls such as
registry.boot_all().
ModuleRegistry
from pymodules import ModuleRegistry
registry = ModuleRegistry(
modules_path="modules", # local module folder
scan_on_init=True, # call scan() during __init__
include_entry_points=False, # include installed package modules
entry_point_group="pymodules.modules", # entry-point group to scan
discovery_order=("filesystem", "entry_points"), # precedence order
duplicate_policy="error", # "error" | "prefer-first" | "prefer-last"
)
Typical lifecycle calls:
registry.discover() # scan sources
registry.resolve() # validate dependency order
registry.instantiate() # create typed modules (if any)
registry.register_all() # run register phase
registry.boot_all() # run boot phase
# Legacy shortcut still supported:
registry.boot() # register_all + boot_all
| Method / Property | Returns | Description |
|---|---|---|
registry.discover() |
None |
v2 alias for scan() |
registry.resolve() |
list[str] |
Dependency-ordered enabled module names |
registry.instantiate() |
None |
Instantiate typed modules from module_class |
registry.register_all() |
None |
Register providers + typed modules |
registry.boot_all() |
None |
Boot typed modules + providers |
registry.shutdown_all() |
None |
Shutdown typed modules + providers (reverse order) |
registry.all() |
list[Module] |
All modules (enabled + disabled) |
registry.all_enabled() |
list[Module] |
Only enabled modules |
registry.all_disabled() |
list[Module] |
Only disabled modules |
registry.find(name) |
Module |
Find by name, raises ModuleNotFoundError |
registry.exists(name) |
bool |
Check if a module exists |
registry.count() |
int |
Total number of modules |
registry.enable(name) |
None |
Enable a module (persists to disk) |
registry.disable(name) |
None |
Disable a module (persists to disk) |
registry.boot() |
None |
Boot all enabled modules |
registry.scan() |
None |
Re-scan the modules directory |
registry.scan_entry_points() |
None |
Scan package entry points (when enabled) |
registry.add(point, value, module=...) |
None |
Add one extension value |
registry.add_many(point, values, module=...) |
None |
Add multiple extension values |
registry.extensions(point) |
list[Any] |
Get values for one extension point |
registry.extension_map(point) |
dict[str, list[Any]] |
Get values grouped by module |
registry.module_path(name, *parts) |
Path |
Resolve path within a module |
registry.on_boot(fn) |
fn |
Register a boot hook (decorator) |
"Blog" in registry |
bool |
Membership test |
for m in registry |
— | Iteration |
Discovery options explained:
include_entry_points=False: safest default; no package-discovered modules.discovery_order=("filesystem", "entry_points"): source precedence.duplicate_policy="error": fail fast on duplicate module keys.
End-to-end: v1 app to v2 app (incremental)
Start with existing v1 behavior:
from pymodules import ModuleRegistry
registry = ModuleRegistry(modules_path="modules")
registry.boot() # existing provider-based modules keep working
Then migrate one module to typed mode:
{
"name": "Billing",
"enabled": true,
"module_class": "modules.Billing.runtime.BillingModule",
"providers": []
}
Use explicit staged lifecycle if you want tighter control:
registry = ModuleRegistry(modules_path="modules")
registry.discover()
registry.resolve()
registry.register_all()
registry.boot_all()
Enable package-based modules later, when needed:
registry = ModuleRegistry(
modules_path="modules",
include_entry_points=True,
discovery_order=("filesystem", "entry_points"),
duplicate_policy="error",
)
registry.boot()
Module
| Property / Method | Returns | Description |
|---|---|---|
module.name |
str |
Module name |
module.path |
Path |
Absolute path to module directory |
module.import_path |
str |
Dotted Python import path |
module.is_enabled |
bool |
Current enabled state |
module.enable() |
None |
Enable (writes module.json) |
module.disable() |
None |
Disable (writes module.json) |
module.version |
str |
Version from manifest |
module.description |
str |
Description from manifest |
module.author |
str |
Author from manifest |
module.providers |
list[str] |
Provider dotted paths |
module.module_class |
str | None |
Optional typed module class path |
module.manifest |
dict |
Raw manifest dict |
module.has_file(*parts) |
bool |
Check if file exists inside module |
module.import_submodule(path) |
module |
Import a submodule by relative dotted path |
Typed Module Contracts
from pymodules import BaseModule, ModuleMeta
class BillingModule(BaseModule):
meta = ModuleMeta(name="Billing", key="billing", version="1.0.0")
def register(self, registry):
registry.add("routes", "billing-route", module="Billing")
def boot(self, app=None):
pass
def shutdown(self, app=None):
pass
v1 Compatibility Helpers
These are available for migration and advanced integration:
LegacyProviderAdapterload_legacy_provider(...)
DjangoModuleRegistry
Extends ModuleRegistry with:
| Method | Returns | Description |
|---|---|---|
registry.installed_apps() |
list[str] |
Dotted app paths for INSTALLED_APPS |
registry.url_patterns() |
list |
URL patterns from all module routes.py files |
registry.migration_modules() |
dict[str, str] |
Dict for MIGRATION_MODULES setting |
registry.collect_settings() |
dict |
Merged dict of all module-level settings |
detect_framework()
from pymodules import detect_framework
info = detect_framework(search_path=None) # defaults to cwd
# info.name → "django" | "fastapi" | "flask" | "unknown"
# info.preset → "django" | "fastapi" | "flask" | "default"
# info.confidence → "high" | "medium" | "low"
# info.reason → str
# info.all_detected → list[str]
Exceptions
from pymodules import (
PyModulesError, # base exception
ModuleNotFoundError, # registry.find("Nonexistent")
ModuleAlreadyExistsError,# generator.generate("Blog") when it exists + no force
ModuleDisabledError, # raised when accessing a disabled module's services
)
Feature Status
✅ Done — v0.1.0
Core
-
ModuleRegistry— scan, boot, find, enable, disable, iterate -
Module— manifest, enable/disable state, import helpers, path helpers -
module.jsonmanifest with versioning, providers, publishes -
ServiceProviderbase class withregister()andboot()hooks - Boot hooks via
registry.on_boot(fn)decorator - Custom folder naming —
modules/,plugins/,apps/,src/features/— anything - Typed exceptions (
ModuleNotFoundError,ModuleAlreadyExistsError)
Framework Detection
- Three-layer detection: venv import → file fingerprints → dependency scanning
-
DJANGO_SETTINGS_MODULEenv var detection - Priority resolution when multiple frameworks present (Django > FastAPI > Flask)
-
detect_framework()public API -
pymodules detectCLI command with confidence display
CLI (pymodules)
-
pymodules init— project setup with auto-detection -
pymodules make <n>— scaffold with preset auto-detection -
pymodules list— table view with enable/disable filter -
pymodules enable/pymodules disable -
pymodules show— full module details -
pymodules delete— with confirmation prompt -
pymodules publish— asset/config publishing with groups -
pymodules detect— framework detection report -
pymodules presets— list all presets -
--modules-pathglobal flag +PYMODULES_PATHenv var -
pymodules.tomlproject config with walk-up discovery
Scaffold Presets
-
plainpreset —module.json+__init__.py -
defaultpreset — framework-agnostic with providers, config, tests -
djangopreset — models, views, apps.py, admin, serializers, migrations -
django-apipreset — DRF serializer/viewset/router scaffolding -
fastapipreset — APIRouter, Pydantic schemas, service layer -
fastapi-crudpreset — full CRUD APIRouter with update/delete skeletons -
flaskpreset — Blueprint, service layer -
flask-apipreset — full JSON CRUD Blueprint scaffolding -
{folder}token in stubs — providers path always correct regardless of folder name
Django Integration
-
DjangoModuleRegistry—installed_apps(),url_patterns(),api_url_patterns(),migration_modules(),collect_settings(),collect_policies() -
AppConfigauto-detection fromapps.py - Module-level URL routing with custom
prefix - Module-level migrations via
MIGRATION_MODULES - Module-level settings merged with
collect_settings() - DRF access policy discovery from module
policies.py/policies/ - Dependency-aware startup ordering with warning fallback during Django settings evaluation
-
"pymodules"as Django app formanage.pycommand discovery -
manage.py module_make— with--preset,--force, smart default todjango -
manage.py module_list— with--enabled/--disabled -
manage.py module_enable/module_disable -
manage.py module_show -
manage.py module_delete— with--yesflag -
manage.py module_publish— with--group,--force -
manage.py module_make_model— lightweight per-file model scaffold -
manage.py module_make_serializer— DRF serializer scaffolding -
manage.py module_make_viewset— DRF ViewSet scaffolding -
manage.py module_make_api_urls— DRF router URL scaffolding -
manage.py module_make_model_migration— migration generation scoped to one model -
manage.py module_make_migration— module-scopedmakemigrations -
manage.py module_migrate— module-scopedmigrate
Dependencies & Boot Order
-
module.jsonrequiresdeclarations - Topological boot ordering for enabled modules
- Circular dependency detection
- Missing / disabled dependency errors
Flask Integration
-
FlaskModuleRegistry— auto-registers Blueprints - App factory pattern via
init_app(app) - Multiple blueprints per module via
blueprintslist
FastAPI Integration
-
FastAPIModuleRegistry— auto-includes APIRouters - App factory pattern via
init_app(app) - Multiple routers per module via
routerslist
Tests
- Core registry tests (scan, find, enable, disable, iterate, exists)
- Module tests (manifest, import path, enable persistence, has_file)
- Generator tests (scaffold creation, preset selection, force, duplicate detection)
- Detector tests (per-framework, confidence levels, priority, env var, dep scanning)
🔄 In Progress
-
pymodules upgradecommand — diff existing module against current preset stubs and offer selective updates - Test coverage enforcement —
pytest-covintegration with minimum threshold config inpymodules.toml - Detector:
setup.pyscanning — legacy projects that don't usepyproject.tomlorrequirements.txt
Roadmap to Industry Standard
The following is what separates a useful personal tool from a package the Python community adopts as a standard.
Documentation & Developer Experience
- Dedicated documentation site (MkDocs + Material theme) — full tutorials, how-to guides, API reference, migration guides
- Interactive quickstart —
pymodules initwalks the user through setup with prompts rather than silent file creation - VS Code extension — syntax highlighting for
module.json, module tree sidebar, right-click "Create module" in explorer - PyCharm / IntelliJ plugin — same as above for JetBrains IDEs
- Error messages with fix suggestions — every raised exception includes the exact command or code to resolve it
Architecture & Core
- Module versioning and constraints —
requires: [{"name": "Auth", "version": ">=1.2.0"}] - Async boot support —
async def register()/async def boot()in service providers for async frameworks (FastAPI, Starlette, Litestar) - Module events / hooks system — typed inter-module communication without direct imports (
module.emit("user.created", payload)) - Lazy loading — modules only fully imported when first accessed, not at boot time
- Hot reload — detect
module.jsonchanges at runtime and re-enable/disable without restart (development mode)
CLI & Tooling
-
pymodules upgrade— compare existing module files against current preset stubs, show diff, offer selective updates -
pymodules lint— validate allmodule.jsonfiles, check for broken provider paths, missing__init__.py, import errors -
pymodules graph— render module dependency graph in the terminal (or export to Mermaid/DOT) -
pymodules test <ModuleName>— run tests scoped to a single module -
pymodules stats— line counts, test coverage, complexity per module
Framework Support
- Litestar integration — routers, dependency injection, middleware
- Starlette integration — Mount-based routing
- Celery integration — auto-discover tasks from enabled modules
- SQLAlchemy integration — auto-collect models and migrations (Alembic)
- Django REST Framework — auto-register routers from module
api.py - Django REST Framework access policies — auto-discover
AccessPolicyclasses from modulepolicies.py - Django Channels — WebSocket routing from module
consumers.py
Testing
- Integration tests — full Django, FastAPI, Flask app boot tests
- Snapshot tests — assert generated scaffold files match expected output exactly
- Matrix CI — test Python 3.10, 3.11, 3.12, 3.13 × Django 4.x/5.x × FastAPI × Flask
- Mutation testing —
mutmutto verify test suite quality - 100% line coverage on core (
module.py,registry.py,generator.py,detector.py)
Distribution & Community
- PyPI release pipeline — build +
twine check+ GitHub Actions publish workflow - GitHub Actions CI/CD — test matrix plus release publish workflow
- Changelog —
CHANGELOG.mdfollowing Keep a Changelog format - Contributing guide — how to add presets, integrations, CLI commands
- Custom preset support — let users define their own presets in
pymodules.tomlpointing to a local stubs directory - Plugin API — third-party packages can register new presets, CLI commands, and integrations via entry points
- Security policy —
SECURITY.md, responsible disclosure process - Benchmarks — boot time with N modules, import overhead vs flat structure
Production Readiness
-
pymodules.lock— deterministic module state snapshot (which modules are enabled, at what version) for reproducible deployments - Module health checks —
registry.health()verifies all enabled modules can import cleanly without actually booting them - Structured logging — opt-in verbose boot logging with module name, provider, timing
- Type stubs —
py.typedmarker, complete__init__.pyifor IDE autocomplete on all public APIs - Thread safety — registry operations safe under concurrent access (WSGI multi-thread, Gunicorn workers)
Contributing
Contributions are welcome. Please open an issue first to discuss what you would like to change.
git clone https://github.com/tomcroot/pymodules
cd pymodules
pip install -e ".[dev]"
pytest
License
MIT — see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pytmodules-0.2.0.tar.gz.
File metadata
- Download URL: pytmodules-0.2.0.tar.gz
- Upload date:
- Size: 74.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ca30a4c56052f2e5e15d0800d5d5b5279c3868b79185e46b878e331eedb58b5
|
|
| MD5 |
d78e7d1561760ff0e4a6ddd9cefab1d4
|
|
| BLAKE2b-256 |
e8af9744f00ff09d18c1df40a49e4528e613f21f1ff11654f2fb6a62c269664f
|
Provenance
The following attestation bundles were made for pytmodules-0.2.0.tar.gz:
Publisher:
publish.yml on tomcroot/pymodules
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytmodules-0.2.0.tar.gz -
Subject digest:
5ca30a4c56052f2e5e15d0800d5d5b5279c3868b79185e46b878e331eedb58b5 - Sigstore transparency entry: 1189224767
- Sigstore integration time:
-
Permalink:
tomcroot/pymodules@3e32c3834477dac5e23daa3849f2cdb9902d7e88 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tomcroot
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3e32c3834477dac5e23daa3849f2cdb9902d7e88 -
Trigger Event:
workflow_run
-
Statement type:
File details
Details for the file pytmodules-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pytmodules-0.2.0-py3-none-any.whl
- Upload date:
- Size: 64.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da3ae1dc743133e73a39aaf92d04cefeec35fa51b95eb9addc88ff5b017210b1
|
|
| MD5 |
25c23aa5d01f19e4f6c91ce7091562c6
|
|
| BLAKE2b-256 |
1062c55b9af46a23f9c5b0cf308f06ebba0350a3b06e7dbf1d686c334fb8da9b
|
Provenance
The following attestation bundles were made for pytmodules-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on tomcroot/pymodules
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pytmodules-0.2.0-py3-none-any.whl -
Subject digest:
da3ae1dc743133e73a39aaf92d04cefeec35fa51b95eb9addc88ff5b017210b1 - Sigstore transparency entry: 1189224772
- Sigstore integration time:
-
Permalink:
tomcroot/pymodules@3e32c3834477dac5e23daa3849f2cdb9902d7e88 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/tomcroot
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@3e32c3834477dac5e23daa3849f2cdb9902d7e88 -
Trigger Event:
workflow_run
-
Statement type: