17 KiB
General Review Guidelines
Derived from real code review history across three services:
- planogrammer — 128 MRs, 954 comments
- cashtrack — 61 MRs, 238 comments
- assetor — 28 MRs, 195 comments
Rules confirmed across multiple services are marked with the services that evidence them. Newer reviews take precedence over older ones within each service.
Code Style & Naming
-
DO name variables to reflect their precise meaning. The name should answer "what is this a collection of?" Prefer
non_existing_idsovermissing,changed_column_idsoverchanges. (planogrammer) -
DO name boolean variables so they read as a statement:
is_changed = column.id in changed_column_ids(planogrammer)
-
DO simplify boolean assignments. Instead of an
if/elseblock that setsTrue/False:# Before if source == PlanogramChangeRequestSource.AI: machine.planogram_optimized_by_ai = True else: machine.planogram_optimized_by_ai = False # After machine.planogram_optimized_by_ai = source == PlanogramChangeRequestSource.AI(planogrammer)
-
DO decompose complex boolean comparisons into named intermediate variables:
are_products_equal = layout_column.product is None or layout_column.product == machine_column.product are_recipes_equal = layout_column.recipe is None or layout_column.recipe == machine_column.recipe return are_products_equal and are_recipes_equal and ...(planogrammer)
-
DO use the
X | Nonesyntax for optional type hints:column_filter: PushPlanogramColumnFilterDto | None = None(planogrammer)
-
DO use
Iterable[T]for input parameters that only need to be iterated, notList[T]. Accept the most general type; return the most specific type. (assetor) -
DO use typed dataclasses for internal structured return values and error payloads rather than plain dicts or multi-value tuples:
# AVOID (asset_per_device_id, company_brand_ids, failed_responses) = await manager.bulk_create_preload(...) failed_responses.append({"device_id": device.id, "reason": "..."}) # DO instead @dataclass class BulkPreloadResult: asset_per_device_id: dict failed_responses: list[FailedResponse] @dataclass class FailedResponse: device_id: int reason: strConsider static factory methods for repeated reason strings:
FailedResponse.for_device_already_has_asset(device_id). (assetor) -
DO prefix limit/capacity constants with
MAX_, e.g.MAX_GET_PRODUCT_BULK_IDS = 1000. This signals that the constant is an upper bound, not a config value. (planogrammer) -
DO suffix all custom exception classes with
Exception, e.g.UnableToDeleteColumnException,BulkLimitExceededException. (planogrammer) -
DO use single
_prefix for protected helpers, double__prefix for private module-level functions (name mangling). The convention is:_= protected,__= private. (planogrammer) -
DO prefer named-constant arithmetic for time durations:
SHORT_PRICE_REVERT_PERIOD = 24 * HOURS # readable # not: 86400 # magic number # not: 60 * 60 * 24 # still unclear at a glance(planogrammer)
-
DO extract repeated small operations into named helper functions. This removes duplication and makes the calling code read at a consistent level of abstraction:
def _clear_changed_prices(column): ... def _get_price_or_none(price): ...(planogrammer)
-
DO invert conditionals to enable early-return/continue patterns:
if not is_changed: _clear_changed_prices(column) continue # handle the positive case at normal indentation(planogrammer)
-
AVOID generic update helpers that accept
Any. A signature like_update_field(name: str, value: Any)eliminates type-checking. Use individual named methods:# AVOID def _update_field(self, name: str, value: Any) -> None: setattr(self._conform, name, value) # DO instead def update_status(self, status: ConformStatus) -> None: ... def update_note(self, note: str) -> None: ...(cashtrack)
-
DO include the type in field names when multiple variants exist or may be added:
# AVOID — breaks when value_token_diff is added later token_diff: Decimal # DO instead vend_token_diff: Decimal(cashtrack)
-
DO keep constants (filter lists, limits, defaults) in a dedicated
const.pyrather than inline in query or view code. (cashtrack) -
DO add schema descriptions for non-obvious query parameters. (cashtrack)
-
DO order method parameters hierarchically:
tenant_idbeforecompany_id, more general before more specific. Apply consistently across all call sites. (assetor) -
AVOID
datetime.utcnow()in new code. Usedatetime.now(UTC)—utcnow()is deprecated in Python 3.12+. (assetor, planogrammer: use project utility module) -
AVOID using
assertin production/runtime code. Assertions are stripped with-O. Raise an explicit exception instead. (planogrammer) -
AVOID tabs; use spaces. Run
ruff formatbefore pushing. Install pre-commit hooks so formatting is enforced automatically on every commit. (planogrammer, cashtrack, assetor) -
AVOID broad ruff/mypy ignore rules in
pyproject.toml. Verify each suppression is genuinely needed by runningpre-commit run --all-files. (assetor)
API Design
-
DO place each new domain in its own folder with its own
endpoints.py,manager.py,dto.py, andexceptions.py. Don't embed new domain read logic in an existing domain's folder. (planogrammer) -
DO add an upper-bound validation on all bulk list inputs:
MAX_GET_PRODUCT_BULK_IDS = 1000 if len(ids) > MAX_GET_PRODUCT_BULK_IDS: raise BulkLimitExceededException(...)(planogrammer, cashtrack)
-
DO enforce input length limits at the API layer to match DB column constraints:
notefields: max 500 charsbarcodefields: max 128 chars (cashtrack)
-
DO use request body (not query params) for mutation inputs. (cashtrack)
-
DO default time-bounded list endpoints to a sensible window when date params are omitted (e.g. last 90 days). Document the default in the endpoint description. (cashtrack)
-
DO make parameters mandatory when they are always required for business logic. Optional parameters that are always supplied create false API flexibility. (planogrammer)
-
DO use mutually exclusive validation when exactly one of several filters must be provided:
@model_validator(mode="after") def validate_exclusive_filter(self) -> "PushFilterDto": provided = sum([bool(self.ids), bool(self.columns), bool(self.view_columns)]) if provided != 1: raise ValueError("Exactly one filter must be provided") return self(planogrammer, cashtrack)
-
DO use the correct HTTP status codes:
404when a referenced resource is not found (not400or500) (assetor)409 Conflictfor business-rule violations unrelated to authorization (not403) (planogrammer)
-
DO for POST/PUT/PATCH/DELETE endpoints, instantiate the repository directly in the endpoint function rather than injecting it as a FastAPI dependency. This ensures transactions roll back correctly on exception. (planogrammer)
-
DO use
company_exists(company_id)when you only need to verify existence; reserveget_companyfor when you need the object's fields. (planogrammer) -
DO write query parameter help text in imperative form. Don't expose internal implementation details (e.g. which database is queried) in OpenAPI docs. (planogrammer)
-
DO keep success responses for void operations minimal — a short
"ok"or empty 204, not a verbose success DTO. (planogrammer) -
DO use SQLAlchemy 2.x style in all new queries:
select(Model)notselect([Model])(condition, value)not[(condition, value)]in CASE expressions- Add
.mappings()to raw SQL results for dictionary-like access (assetor)
-
AVOID kebab-case query parameter names when the rest of the service uses snake_case. (planogrammer)
Error Handling & Exceptions
-
DO place domain-specific exceptions in that domain's own
exceptions.py. If an exception is only raised within one flow, it belongs in that flow's module. (planogrammer) -
DO add a catch-all handler in every endpoint for the base exception class of your domain exceptions. This ensures unhandled domain errors return a structured response rather than 500. (planogrammer)
-
DO narrow
try/exceptblocks to only the lines that can raise the caught exception:# AVOID — business logic inside the try block catches unintended exceptions try: assignment = await self._get_asset_machine(asset) if assignment.machine_id is None: raise AssetItemNotAssigned(asset.id) except ObjectNotFound: raise AssetItemNotAssigned(asset.id) # DO instead try: assignment = await self._get_asset_machine(asset) except ObjectNotFound: raise AssetItemNotAssigned(asset.id) if assignment.machine_id is None: raise AssetItemNotAssigned(asset.id)(assetor)
-
DO log the caught exception (at least at
DEBUGlevel) when swallowing or transforming it, so the original cause remains traceable in logs. (planogrammer) -
DO validate payloads thoroughly before persisting:
- Reject if all denomination/token values are zero
- Reject duplicate entries in the same payload
- Validate numeric values against DB column constraints before hitting the DB (cashtrack)
-
DO handle external service unavailability explicitly. Always add error handling for the unreachable case when calling external services. (assetor)
-
DO skip unknown enum/field values in changelog and audit endpoints rather than raising. (cashtrack)
-
AVOID raising a generic
Exceptionfor domain-specific error conditions. Define a named exception class. (planogrammer) -
AVOID importing the entire
exceptionsmodule — import only the specific class you need. (planogrammer)
Testing
-
DO write tests in the same MR as the feature. (cashtrack, planogrammer)
-
DO assert on IDs only when verifying list/collection responses, not on full DTO content:
assert {r["id"] for r in response_json["content"]} == {p1.id, p2.id}This prevents test breakage when DTO fields are added or renamed. (planogrammer)
-
DO put manager-level behaviour tests in
test_<domain>_manager.py, not in endpoint tests. Endpoint tests should only exercise endpoint-layer code. (assetor) -
DO cover every status transition with a dedicated test. (assetor, cashtrack)
-
DO cover filter behaviour at the endpoint level, including interactions with data-access restrictions. (cashtrack)
-
DO add tests for prices with 4 decimal places — this edge case has caused production bugs. (planogrammer)
-
DO write a serialization test for every response DTO that contains
Decimalordatetimefields:def test_when_model_dump_with_json_mode_then_price_is_float(): dto = MyResponseDto(price=Decimal("1.2345")) result = json.loads(dto.model_dump_json()) assert isinstance(result["price"], float)(planogrammer)
-
DO use standard
setup_method/teardown_methodfor synchronous test setup/teardown. Only useasyncfixtures when you genuinely need toawaitinside them. (planogrammer) -
AVOID hard-coding specific auto-increment IDs in test assertions. Use the IDs from the objects created in test setup. (planogrammer)
Architecture & Domain Rules
-
DO fetch company/entity once per request and pass the object downstream. Never call
get_company(or equivalent) more than once in the same request path. (planogrammer) -
DO move entity existence checks into manager/service methods, not the endpoint layer. (planogrammer)
-
DO batch multiple individual DB queries into a single
listquery where possible. (planogrammer) -
DO decompose large methods that do multiple conceptually distinct things. One method should do one thing. (planogrammer, cashtrack)
-
DO keep manager responsibilities focused on a single flow. Read/GET logic and write logic belong in separate managers. (planogrammer)
-
DO initialize collaborating managers in the constructor, not lazily inside methods. (planogrammer)
-
DO use
save_many(entities)for bulk saves instead of loopingsave(entity). (planogrammer) -
DO pass already-loaded ORM objects to methods rather than re-fetching by ID. (planogrammer)
-
DO prefer explicit
save()calls over relying on implicit ORM dirty-tracking. Explicit saves make the persistence boundary visible. (planogrammer) -
DO prefer a direct field-update query over a fetch-then-modify-then-save pattern when updating a single field. (planogrammer)
-
DO use soft-delete (
aliveflag or equivalent) to preserve historical records. Always filter queries with the soft-delete sentinel — omitting it causes deleted records to leak into results. (cashtrack) -
DO look up by natural key before creating on import operations. Reuse an existing record if it already exists rather than always inserting. (cashtrack)
-
DO recalculate derived totals (e.g.
total_coins,total_bills) from source data whenever source data changes — never accept pre-calculated totals from the caller. (cashtrack) -
DO wire data-access (company + user) restrictions into every new list or filter endpoint before the MR is ready for review. (cashtrack)
-
DO resolve circular imports using
TYPE_CHECKINGguards andfrom __future__ import annotations:from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from televend_core.databases.televend_repositories.asset_item.model import AssetItemUse
@dataclass(stdlib) rather than Pydantic for internal DTOs when forward references cause Pydantic config errors. (assetor) -
AVOID nesting context managers from different sources. Use one transaction boundary per operation. (planogrammer)
-
AVOID the deprecated
load_optionsparameter on repository calls. Use typedLoadOptionsclasses fromtelevend-core. (planogrammer) -
AVOID generalizing too early. If two similar pieces of code would diverge with the next requirement, keep them separate (Rule of Three). (planogrammer)
DTO & Serialization
-
DO implement
from_model(cls, orm_model)as a classmethod on every response DTO. Keep endpoint handlers clean — don't inline model-to-DTO mapping there:@classmethod def from_model(cls, m: MachinePCR) -> "MachinePCRDto": return cls(id=m.id, status=m.status, ...)(planogrammer)
-
DO use
model_dump_json()(notmodel_dump()) when serializing DTOs that containDecimalordatetimefields, to ensure correct JSON types. (planogrammer) -
DO use
ConfigDictconsistently in all Pydantic v2 models. Don't mix oldclass Configstyle withmodel_config = ConfigDict(...)in the same codebase. (planogrammer) -
DO give optional Pydantic fields an explicit
default=None:class MyDto(BaseModel): filter: str | None = None(planogrammer)
Git & Process
-
DO name feature branches
feature/CLOUD-NNNNN-short-description. Thefeature/prefix is required for CI/CD pipeline triggers. (planogrammer, cashtrack, assetor) -
DO title MRs as
CLOUD-NNNNN: description(imperative, present tense). Avoid the "Resolve" prefix auto-generated by GitLab. (cashtrack) -
DO include the full endpoint path in CHANGELOG entries:
- Bad:
Added new endpoint - Good:
POST /v1/tenants/{tenant_id}/companies/{company_id}/planogram-change-requests/push/bulk(planogrammer)
- Bad:
-
DO keep MRs focused on a single ticket. Preliminary or follow-up changes belong in their own MR. (cashtrack)
-
DO rebase your feature branch on
developbefore requesting review. (planogrammer) -
DO resolve all open review threads before merging. (cashtrack, planogrammer)
-
DO ensure ruff passes before requesting review — a cleanup-only "Ruff is happy" MR is a sign that formatting discipline slipped. (cashtrack, assetor, planogrammer)
-
DO add
/.venvto.gitignore. (assetor) -
DO bump the service version in
pyproject.tomlas part of any dependency-upgrade MR. (assetor) -
DO update
README.mdwhen performing a major runtime upgrade. (assetor) -
AVOID
event_loopfixture overrides inconftest.py— no longer required with modernpytest-asyncio. (assetor) -
AVOID
[ci-skip]commits for changelog-only changes unless the change truly has no code impact. (planogrammer)