Features

uvlhub is built as a set of features. Each one lives in its own package under app/features/<name>/ and owns everything it needs: blueprint, routes, models, repositories, services, templates, assets and tests. There is no shared “modules” layer to coordinate with — a feature is self-contained.

This directory used to be app/modules/. The domain word is now feature, not module. The Rosemary commands that operate on them were renamed too: make:module became feature:create, and module:list became feature:list.

Table of contents

  1. Which features are loaded
  2. The features
    1. Core features
    2. Deployment features
  3. How coupled are they
  4. What a feature looks like
  5. Tests
  6. Per-feature environment
  7. Creating a new feature

Which features are loaded

The set of features the app loads is declarative. It lives in the root pyproject.toml under [tool.splent]:

[tool.splent]
features = [
    "auth",
    "dataset",
    "explore",
    "featuremodel",
    "flamapy",
    "hubfile",
    "profile",
    "public",
    "team",
    "zenodo",
]
features_dev = [
    "webhook",
]
features_prod = [
    "webhook",
]

At startup, app/feature_loader.py reads these lists and loads the union of features and features_<env>, where env is prod when the app is created with config_name="production" and dev otherwise, which keeps webhook out of production.

The module-level app = create_app(...) that gunicorn imports passes FLASK_ENV as the config name, so with FLASK_ENV=production the loader resolves the prod set and webhook is not registered. See Feature selection for the full contract and the resolved per-environment table.

A package sitting in app/features/ that is not named in either list is simply skipped. If neither list is declared at all, every package found on disk is loaded.

Where features are selected

The [tool.splent] lists above are the only place features are selected. The canonical reference is Feature selection.

The [tool.splent] lists are the declarative source. To see what the running app actually registered, list its routes:

rosemary route:list

That prints every registered blueprint endpoint with its methods and URL rule, grouped by endpoint prefix, so a feature that failed to load shows up as a missing block of routes.

Ask Rosemary directly which features load:

rosemary feature:list

It resolves them through app/feature_loader.py, the same code the application runs at startup, so the answer cannot drift from what actually loads. Pass --env prod to see the production set:

rosemary feature:list --env prod

Both environments resolve the same eleven features today, since webhook is declared in both env lists so production can serve the deployment endpoint. The command also flags features declared in pyproject.toml but missing from disk, and features sitting in app/features/ that no list declares, so neither mistake stays invisible.

The features

Core features

| Feature | What it does |
|:——–|:————-|
| auth | Signup, login and logout. Owns the User model and the login manager wiring. |
| dataset | Dataset upload, listing, download and DOI resolution (/doi/<doi>/). Also defines the REST resources at /api/v1/datasets/ in api.py. |
| explore | Dataset search at /explore, filtering by query, sorting, publication type and tags. |
| featuremodel | The FeatureModel, FMMetaData and FMMetrics models, linking a dataset to its UVL files. |
| flamapy | UVL validation and format conversion through
flamapy
check_uvl, valid, to_glencoe, to_splot, to_cnf.    
  hubfile Individual file download and view, at /file/download/<id> and /file/view/<id>.
  profile Profile editing at /profile/edit and the signed-in user’s own summary at /profile/summary.
  public The landing page at /.
  team The static team page at /team.
  zenodo Deposition, upload and publication against the Zenodo REST API.

Deployment features

Feature What it does
webhook Receives a deploy POST at /webhook/deploy. Declared in both features_dev and features_prod: development needs it for its test suite, production for the CD pipeline. Refuses every request with a 503 until WEBHOOK_TOKEN is configured.

How coupled are they

Measured from the production code (imports excluding tests, schema foreign keys, and cross-feature url_for references in templates), the eleven features fall into three tiers.

Independent. team and webhook import nothing from other features and nothing imports them. They can be removed from [tool.splent] without breaking anything else.

Clean consumers. public consumes only other features’ services, which is the pattern the layering intends. explore imports dataset and featuremodel models to build its search query, an acceptable read-side dependency, and nothing imports it. flamapy is clean in Python (hubfile services only), but dataset’s detail template links into five of its endpoints, so disabling it breaks that page. Python-decoupled, UI-coupled.

The core. auth and profile import each other at module level and are one unit in practice. dataset, featuremodel and hubfile form a module-level dependency triangle, tied at the schema too: feature_model references data_set, author references fm_meta_data, and hubfile’s records reference user. zenodo imports dataset at module level, and dataset imports zenodo lazily inside the upload flow, so removing zenodo survives import but breaks uploads at call time. Together with auth these six deploy as a unit today.

The dependency matrix, direction is “row imports column”:

  auth dataset featuremodel hubfile profile zenodo
auth       models, repos, services  
dataset services repos repos   services (lazy)
explore   models models      
featuremodel   models services    
flamapy       services    
hubfile models models, services (lazy) models    
profile services repos      
public   services services      
zenodo   models models    

Five of those edges skip the target’s service layer and reach a repository directly (auth to profile, dataset to featuremodel and to hubfile, profile to dataset), and one model imports another feature’s service (hubfile’s Hubfile uses dataset’s SizeService). They work, but they are the edges to unwind first if the product ever needs to derive without part of the core: hold to a services-only rule across features, and guard the cross-feature url_for links in templates behind the feature’s presence.

What a feature looks like

Not every feature has every file — a feature only carries what it needs. public and team are on the small end: a blueprint whose init_feature hook registers their script and nav item with the framework registries, a routes.py, a template, a script under assets/js/, and a tests/ directory — but no models, repositories, services or forms, because they persist nothing. dataset has the full set:

app/features/dataset/
├── __init__.py          # the blueprint
├── api.py
├── assets/
├── forms.py
├── models.py
├── repositories.py
├── routes.py
├── seeders.py
├── services.py
├── templates/
├── tests/
└── uvl_examples/

The loader imports routes, models, hooks and signals if they exist, then registers every Flask Blueprint it finds in the feature root or those submodules. Two optional hooks are also honoured: config.inject_config(app) runs before anything else touches app.config, and init_feature(app) in the feature’s __init__.py runs once the module is imported.

Base classes come from the splent_framework package, not from the repository:

from splent_framework.blueprints.base_blueprint import BaseBlueprint
from splent_framework.repositories.BaseRepository import BaseRepository
from splent_framework.services.BaseService import BaseService

See splent_framework for the full surface.

Tests

Tests live inside the feature, in app/features/<feature>/tests/, one file per layer of the testing pyramid:

app/features/zenodo/tests/
├── test_unit.py
├── test_repository.py
├── test_service.py
├── test_integration.py
└── test_selenium.py

Each file sets its marker at module level, for example pytestmark = pytest.mark.service. The markers unit, repository, service, integration, e2e and load are declared in the root pyproject.toml. Features that carry load tests add a locustfile.py alongside the others.

See Testing for how to run each layer.

Per-feature environment

A feature can ship its own .env.example. Today zenodo is the only one that does, because it is the only feature needing a secret of its own:

app/features/zenodo/.env.example

You copy it to .env next to it, fill it in, and then merge every feature .env into the root one:

rosemary compose:env

That command walks app/features/, collects each .env it finds, and merges the variables into the root .env, warning you about conflicts instead of overwriting. See Composing environment.

Creating a new feature

rosemary feature:create <name>

This scaffolds app/features/<name>/ with a blueprint, routes, models, repositories, services, forms, seeders, an index template, an asset script, and one test file per layer including a locustfile.py.

The command does not touch pyproject.toml. Add the name to [tool.splent] features yourself, otherwise the loader will skip the new feature and none of its routes will exist.

See Create feature.


Table of contents