Skip to content

Package Development

Packages are how first-party and community code plugs into Almasix: a ServiceProvider registers bindings, merges config, loads routes / views / translations / migrations, offers publishable assets, and optionally registers console commands. Scaffold a package inside your app (or a sibling repo), install it editable, and publish its assets:

terminal
smith make:package courier
pip install -e packages/courier
smith vendor:publish --tag=courier-config
# resolve a namespaced view or hit a package route to confirm the provider booted

Every package ships a provider that subclasses almasix.providers.ServiceProvider. Call the helpers from register() (config and bindings) and boot() (routes, views, publishes, commands):

app/console/commands/example_command.py
from pathlib import Path
from almasix.providers import ServiceProvider
_HERE = Path(__file__).resolve().parent
class CourierServiceProvider(ServiceProvider):
def register(self) -> None:
self.merge_config_from(_HERE / "config" / "courier.py", "courier")
def boot(self) -> None:
self.publishes(
{_HERE / "config" / "courier.py": self.app.path("config", "courier.py")},
"courier-config",
)
self.load_routes_from(_HERE / "routes" / "web.py")
self.load_views_from(_HERE / "resources" / "views", "courier")
self.load_translations_from(_HERE / "lang", "courier")
self.load_migrations_from(_HERE / "database" / "migrations")
self.publishes_migrations(
{
_HERE / "database" / "migrations" / "create_courier_messages_table.py": (
"database/migrations/create_courier_messages_table.py"
),
},
"courier-migrations",
)
self.commands([CourierStatusCommand])

Register the provider in the application’s config/app.py providers list, or rely on auto-discovery via entry points.

Installed packages advertise providers with the almasix.providers entry-point group:

pyproject.toml
[project.entry-points."almasix.providers"]
courier = "courier.provider:CourierServiceProvider"

On boot, Application.register_configured_providers registers:

  1. FoundationServiceProvider
  2. Every path in config/app.pyproviders
  3. Every discovered entry point unless discovery is disabled
config/app.py
config = {
"providers": [
"app.providers.app_service_provider.AppServiceProvider",
],
# Skip entry-point discovery entirely.
"skip_provider_discovery": False,
# Distribution names to exclude from auto-discovery.
"dont_discover": ["almasix-spam"],
}

PackageManifest resolves entry points through importlib.metadata. A broken optional package is skipped softly so one bad install cannot take down boot.

Loads a Python config module (config = {...} or top-level names) under a key. Package defaults fill missing keys; values already in the application win (published config overrides package defaults):

examples/package-development.py
self.merge_config_from(_HERE / "config" / "courier.py", "courier")
# config("courier.driver")

Executes a route file through Application.load_route_file — the same path broadcasting uses for routes/channels.py:

examples/package-development.py
self.load_routes_from(_HERE / "routes" / "web.py")

Registers a Prism namespace so view("courier::welcome") resolves against the package tree. Published overrides live under resources/views/vendor/courier/ and win over the package hint. The helper also declares a {namespace}-views publish tag for the whole views directory.

examples/package-development.py
self.load_views_from(_HERE / "resources" / "views", "courier")
from almasix.prism.helpers import render
render("courier::welcome", {"driver": "pigeon"})

Migrations — load_migrations_from / publishes_migrations

Section titled “Migrations — load_migrations_from / publishes_migrations”

load_migrations_from registers directories with the migrator so smith migrate finds package migrations without publishing them. Package sources use slug-only names (create_widgets_table.py) — no YYYY_MM_DD_HHMMSS_ prefix.

publishes_migrations is like publishes, but vendor:publish stamps each destination as YYYY_MM_DD_HHMMSS_{slug}.py (incrementing the second when several migrations publish together) so they sort after the application’s existing migrations.

Registers a language namespace. Catalog files live under {path}/{locale}/{group}.py; keys use namespace::group.key:

examples/package-development.py
self.load_translations_from(_HERE / "lang", "courier")
__("courier::messages.greeting")

Registers Command subclasses on the console kernel when it is bound (providers’ boot() runs after the kernel is available during Smith boots):

app/console/commands/example_command.py
self.commands([CourierStatusCommand])

publishes / publishes_migrations only declare paths. Users copy them with:

terminal
smith vendor:publish --tag=courier-config
smith vendor:publish --provider=courier.provider.CourierServiceProvider
smith vendor:publish --all

Convention for tags: {package}-config, {package}-migrations, {package}-views, {package}-lang. See Smith Console for the full vendor:publish surface.

Kind Convention
First-party Almasix packages Python: almasix.* (Signet, Prism, …). JS clients: @almasix/* (Sonar).
Third-party / community Own distribution name (almasix-courier, acme-billing); do not claim the almasix. import namespace.
Config / publish tags Package short name: courier, courier-config.
View / lang namespaces Same short name: courier::welcome, courier::messages.greeting.
terminal
smith make:package courier
smith make:package acme-billing --path=packages/billing

Creates under packages/{name}/ (or --path):

  • pyproject.toml with an almasix.providers entry point
  • src/{module}/provider.py with sample merge_config_from / load_* / publishes calls
  • Config, routes, views, lang, and migration stubs
  • A README with install and publish notes
  • Unit-test the provider by constructing it with an Application(tmp_path), calling register() / boot(), and asserting config, routes, view resolution, and publish tags (ServiceProvider.forget_publishes() between tests).
  • Prefer almasix.testing.TestCase / boot_application() when the package needs a full app boot — see Testing.
  • After pip install -e, resolve a namespaced view (courier::welcome) or hit a package route from your app to confirm discovery and publishing work.
Piece Path
Base provider src/almasix/providers/provider.py
Discovery src/almasix/providers/package_manifest.py
View namespaces src/almasix/prism/engine.pyadd_namespace / :: finder
Migration paths src/almasix/orm/migration.pyregister_migration_paths
Generator smith make:packagealmasix.smith.make.make_package
Scaffold output packages/{name}/ in your app or repo