Skip to content

Custom python linters

See: Notion page ⧉

NoQA codes

ALN002

Checks that we don't import components in shared

ALN004

Checks that we don't import apps in shared

ALN009

Checks that models do not import business logic

ALN010

UUID foreign keys must be called as UUID(as_uuid=True)

ALN011

cache.memoize() must always have a timeout argument to make them eligible for eviction by our current redis policy"

ALN014

Checks that modification of attributes, or instantiation of policies, enrollments, employments and exemptions are only done inside the fr health insurance affiliation component only

ALN015

Checks that we don't call legacy user lifecycle functions

ALN016

logger can't use keyword arguments

ALN017

Do not use top of file imports for business_logic or services in controllers, admin_tools or models, unless for typing. See this Notion page : https://www.notion.so/alaninsurance/Imports-and-start-up-time-WIP-5495c8713847434986ef03ad09158bf1 ⧉

ALN018

Do not put python code to init files. It's executed when the module is loaded, it can generate weird side effects, and slow the loading of the application

ALN021

Foreign keys must use models, not string. See this Notion page : https://www.notion.so/alaninsurance/Imports-5495c8713847434986ef03ad09158bf1 ⧉

ALN024

Single health contract is deprecated: a company can have multiple contracts. Prefer using company.contracts.

ALN025

Prevents prevoyance_claim_management to import claim_management

ALN026

Prevents claim_management to import prevoyance_claim_management

ALN027

This class checks that no protected attribute access is used.

ALN030

@command_XXX must be after @XXX.command

ALN032

Checks module names

ALN034

While the shareable feature code lives in fr_api, you should consider it as a standalone feature and not as a part of Alan France. You should avoid imports from other features and other modules of apps/fr_api. See https://www.notion.so/alaninsurance/Building-shareable-features-0c4a2681e88d4b8c8e68aaaf66794e99 ⧉",

ALN037

@property are not serialized by dataclass_json. If not a problem for your use case, please ignore (# noqa: ALN037). "See this Notion page: https://www.notion.so/alaninsurance/Dataclass-the-good-parts-8f387ec2a8e24a55af294237b97dc210#a611a9154af448a38e552c61415169df ⧉

ALN038

ALN038: Calling tracer.{func_name} directly is not allowed. Please use shared.helpers.tracing.tag_current_request_root_span instead. Refer to https://www.notion.so/alaninsurance/Performance-Monitoring-APM-50bda306a47446a3912cc1eb10977335#25eb69a5452040d68f512f2cded9970b ⧉ to understand why in more details.

ALN039

The module public cannot import other components (or from apps/), it's meant to be the public interface of the component #{self.component_name}, see: https://www.notion.so/alaninsurance/Modular-monolith-a9d84f1318d34115bbd08b3648e5a587 ⧉",

ALN041

As the model '{node.name}' is in a component it must be part of specific schema that is declared in components/{self.component_name}/models/helpers.py",

ALN042

1/n or n/1 relationships must define an explicit 'order_by' clause.

ALN043

You can only import things from a Component public module, see: https://www.notion.so/alaninsurance/Modular-monolith-a9d84f1318d34115bbd08b3648e5a587 ⧉",

ALN045

HealthContract.query should not be called directly outside from the contracts_management/ module. Please check the guidelines: https://www.notion.so/alaninsurance/Contract-management-module-WIP-405f318eae714430a9e29ce40b2811bb?pvs=4#0ea62ff6f41148c5a385f7bc94a6b250 ⧉

ALN046

to use owner_only strategy the class must be wrapped with the @with_ownership decorator

ALN047

ForeignKey columns must be indexed. Please add index=True to the Column definition and make sure the migration is created

ALN049

app is a globally shared fixture with the same controllers as the production one, registering blueprints will impact other tests

ALN051

Check that we use isodate_field(), optional_isodate_field(), isodatetime_field() and optional_isodatetime_field() in json dataclasses for es/be/ca apps

ALN053

Importing a model at the top of load_all_models is not allowed as it can break the versioning manager. All imports should be made in the load_all_models method after the versioning manager call.

ALN054

Migrations should not raise exceptions. Please update raise_when_too_many_locks to False before committing

ALN055

Files containing tests should be prefixed with 'test_' or tests won't run!

ALN056

freeze_time as a function decorator isn't working well with pytest, you should convert it to a context manager

ALN057

Don't use context managers (aka with xxx) with mocker in pytest. They do not work as expected.

ALN058

import inside freeze_time context managers can break the imported module

ALN059

Checks that all component dependencies are declared in the root of a component

ALN061

invalid secret name - it must follow the defined naming scheme https://www.notion.so/alaninsurance/Using-AWS-Secrets-Manager-1bb472e7a620429ab362052c91b729a4?pvs=4#0e4fae3a55b74b08b3651efabcb2cc12 ⧉

ALN062

Opening and closing curly braces in strings don't match

ALN063

Don't use the @dataclass_json decorator, use the DataClassJsonMixin instead, as it provides better typing.

ALN064

Default factory fields should not contain date or datetime objects, Please use factory.LazyFunction or factory.LazyAttribute instead

ALN065

This checker is used to ensure that the decryption of medical conversation messages can only be done in the dedicated clinic_ai.py file Decided here: https://github.com/alan-eu/Topics/discussions/25049?sort=old#discussioncomment-8569509 ⧉ (item 5)

ALN066

Using strings to indicate column or relationship paths in loader options is deprecated and will be removed in SQLAlchemy 2.0. Please use the class-bound attribute directly.

ALN068

cached() must always have an expire_in argument to make them eligible for eviction by our current redis policy"

ALN069

You can't import models from other components, see: https://www.notion.so/alaninsurance/Modular-monolith-a9d84f1318d34115bbd08b3648e5a587 ⧉",

ALN070

import inside freeze_time context managers can break the imported module

ALN071

SQLAlchemy v2 migration, in models all attributes (or functions with @declared_attr) must return a Mapped[...] type. If it's a class variable type it with ClassVar[...]

ALN072

Functions decorated with @enqueueable should not use non-primitive type arguments. The function is used by RQ and the arguments must be serializable and robust for code moves/refactors.

ALN073

Don't call functions without assigning their return value to a variable.

ALN076

The tablename class attribute is mandatory on all SQLAlchemy models inheriting from DbModel, AlanNonVersionedModel, BaseModel or any of their derived classes.

ALN077

Check for usage of backref in SQLAlchemy relationships and suggest using back_populates instead.

ALN079

Check for direct instantiation of AlanBaseFactory or its derived classes. Always use the .create() class method instead of direct instantiation.

ALN080

The Model.query.get(...) method is considered legacy as of the 1.x series of SQLAlchemy and will be removed. Use " current_session.get(Model, ...) instead.

ALN083

Flake8 plugin to check ProfileService constructor usage.

ALN084

Prevents the use of Flask's current_app.config. Use shared.helpers.config.current_config instead.

ALN085

Enforces SQLAlchemy 2.0 query style by detecting deprecated patterns that will be removed in SQLAlchemy 2.0.

This linter checks for: 1. Usage of session.query() which is deprecated in favor of session.scalar(), session.scalars(), or session.execute()

Note: Other SQLAlchemy 2.0 deprecation warnings are already being turned into errors at runtime by the _turn_sql_alchemy_warning_into_errors function in backend/shared/models/orm/sqlalchemy.py.

ALN086

Enforces the use of mapped_column instead of Column in SQLAlchemy 2.0+ models.

This linter checks for usage of Column() calls which should be replaced with mapped_column().

In SQLAlchemy 2.0+, Column is deprecated in favor of mapped_column for better type annotation support and modern SQLAlchemy patterns.

Note: Migration files (in directories containing "migrations") and Column() calls inside Table() definitions are excluded from this check as they legitimately need to use Column().

ALN087

Suppress SQLAlchemy query tracker warnings for N+1 queries and unused eager loads.

This linter serves as a marker to suppress specific SQLAlchemy query optimization warnings related to N+1 queries When the SQLAlchemy query tracker detects potential N+1 queries, it will check for the presence of this linter (ALN087) at the exact line number where the issue occurs. If found, the warning will be suppressed.

ALN088

Prevents the use of unless=is_test_mode in cache decorators. This pattern changes behavior between test and production environments, which is not a best practice.

ALN089

pytest fixtures with autouse=True in conftest.py files can have unwanted side-effects as they will be used in any tests in the subfolders and can inadvertently use other fixtures that add functionality. Instead, mark tests that need the fixture explicitly with @pytest.mark.usefixtures("fixture_name") or use the fixture as a parameter.

ALN090

All modular monolith components must have an observability.py file with a ServiceObservability instance. This ensures all components are properly instrumented for observability.

ALN091

All API endpoint functions in components/*/public/ must have @obs.api_call decorator. This ensures all public API endpoints are properly instrumented for observability.

ALN092

Suppress SQLAlchemy query tracker warnings for N+1 queries and unused eager loads.

This linter serves as a marker to suppress specific SQLAlchemy query optimization warnings related to N+1 queries When the SQLAlchemy query tracker detects potential unused eager loads, it will check for the presence of this linter (ALN092) at the exact line number where the issue occurs. If found, the warning will be suppressed.

ALN093

Prevent usage of current_logger from shared.helpers.logging.logger

ALN094

Logs with audit_event_type or audit_event_scope must use current_audit_logger, not current_logger.

ALN095

Check that functions passed to .enqueue() have @enqueueable decorator.

This rule catches a common error where developers forget to add the
@enqueueable decorator to functions that will be enqueued as jobs.
The runtime error only occurs when the job is actually enqueued,
which may be in a rarely-executed code path.

ALN096

Do not set auth headers (Authorization, CF-Access-*, X-Api-Key) inline in HTTP calls.

Use a requests.auth.AuthBase subclass instead. This centralizes auth logic, makes credentials easier to rotate, and prevents accidental leaks in logs.

Bad:

requests.get(url, headers={"Authorization": f"Bearer {token}"})

Good:

class BearerAuth(requests.auth.AuthBase):
    def __init__(self, token: str) -> None:
        self.token = token

    def __call__(self, r: requests.PreparedRequest) -> requests.PreparedRequest:
        r.headers["Authorization"] = f"Bearer {self.token}"
        return r

requests.get(url, auth=BearerAuth(token))

ALN097

Google API .execute() calls must pass num_retries for resilience against rate limiting.

ALN098

Flake8 plugin to prevent direct instantiation of deprecated Marshmallow field types.

Marshmallow 3.24 deprecated: - fields.Field -> use fields.Raw instead - fields.Mapping -> use fields.Dict instead - fields.Number -> use fields.Float or fields.Int instead

Note: Inheriting from these types (e.g., class MyField(fields.Field)) is still allowed, only direct instantiation (e.g., fields.Field()) is forbidden.

ALN099

Do not reference profile_id in data model, should use global_user_id instead.

profile_id is being retired in favor of gloabl_user_id (=user.id as string). For more details, see: https://github.com/alan-eu/Topics/discussions/31615 ⧉

ALN100

Detects .options(...) calls that mix two different loader strategies on the same relationship path.

SQLAlchemy raises "Loader strategies for ORM Path(...) conflict" at query-compile time when two different loaders target the same attribute (e.g. joinedload(X.y) and contains_eager(X.y)). The error only surfaces at runtime when the query is compiled against a real engine; unit tests that mock the session never trigger it. This rule catches the pattern statically.

Same-strategy duplicates (joinedload(X.y), joinedload(X.y)) are tolerated — SQLAlchemy merges them. Chained loaders are tracked as a full path (joinedload(A.x).joinedload(B.y) keys on A.x -> B.y), so siblings differing only by their parent relationship do not collide.

ALN101

Flag # noqa: ALN### directives whose ALN rule does not fire on the line.

ALN102

In test files, don't pass init_versioning_manager to load_all_models_from_all_components — rely on the default (True). init_versioning_manager=False loads every component's models (incl. versioned ones like FR command_log) but skips grafting their dynamic versioning columns (actor_id, auth_context_id); create_all then builds those tables WITHOUT the columns and caches the schema for the whole xdist worker, so later tests that write to them fail with "column actor_id of relation command_log does not exist" (flaky, EP-10059). init_versioning_manager=True is redundant (it is the default). Remove the argument.

ALN103

Do not put secrets on a hardcoded custom-scheme deep link.

Custom URL schemes (alanmobile://, alanbemobile://, alanesmobile://) can be registered by any app on the device, which can then hijack the link and read everything in it. So a secret query parameter on such a link can leak to a malicious app → token theft / account takeover.

This guard only flags hardcoded string/f-string literals that carry both a custom scheme and a forbidden query parameter — the one pattern that is custom scheme in every environment (it bypasses DEEP_LINK_BASE_URL, which is https in prod). The build_deep_link(query_args={"token": ...}) path is intentionally not flagged: it resolves to an https Branch/Universal Link in prod.

Bad:

deeplink = f"alanmobile://impersonate/?user_id={uid}&admin_token={token}"

Good:

url = build_url(key, base_url=current_config["FR_FRONT_END_BASE_URL"],
                query_args={"token": token})  # https://link.alan.com/...

Known gaps (rare, accepted): string concatenation with + across literals, and a forbidden param injected only through an interpolated variable (e.g. f"alanmobile://{href}") are not statically visible.

ALN104

Check for direct SaleorClient(...) construction outside the getter module.