Skip to content

Gears API reference

Gears

Gear module for managing user equipment.

This module provides CRUD operations and data models for user gear tracking including bikes, shoes, wetsuits, racquets, skis, snowboards, windsurf, and water sports equipment.

Exports
  • CRUD: get_gear_user_by_id, get_gears_number, get_gear_users_with_pagination, get_gear_user, get_gear_user_contains_nickname, get_gear_user_by_nickname, get_gear_by_type_and_user, get_gear_by_strava_id_from_user_id, get_gear_by_garminconnect_id_from_user_id, create_multiple_gears, create_gear, edit_gear, delete_gear, delete_all_strava_gear_for_user, delete_all_garminconnect_gear_for_user
  • Schemas: GearBase, GearCreate, GearRead, GearUpdate, GearsListResponse
  • Enums: GearType
  • Models: Gear (ORM model)

GearBase

Bases: BaseModel

Base model for gear data.

Attributes:

Name Type Description
brand StrictStr | None

Gear brand name.

model StrictStr | None

Gear model name.

nickname StrictStr

Gear display nickname.

gear_type GearType

Gear type identifier.

created_at datetime | None

Gear creation timestamp.

active StrictBool | None

Whether the gear is active.

initial_kms StrictFloat | None

Initial kilometers.

purchase_value StrictFloat | None

Purchase value.

strava_gear_id StrictStr | None

Strava gear ID.

garminconnect_gear_id StrictStr | None

Garmin gear ID.

Source code in backend/app/modules/gears/gear/schema.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class GearBase(BaseModel):
    """
    Base model for gear data.

    Attributes:
        brand: Gear brand name.
        model: Gear model name.
        nickname: Gear display nickname.
        gear_type: Gear type identifier.
        created_at: Gear creation timestamp.
        active: Whether the gear is active.
        initial_kms: Initial kilometers.
        purchase_value: Purchase value.
        strava_gear_id: Strava gear ID.
        garminconnect_gear_id: Garmin gear ID.
    """

    brand: StrictStr | None = Field(
        default=None,
        max_length=250,
        description="Gear brand name",
    )
    model: StrictStr | None = Field(
        default=None,
        max_length=250,
        description="Gear model name",
    )
    nickname: StrictStr = Field(
        ...,
        max_length=250,
        description="Gear display nickname",
    )
    gear_type: GearType = Field(
        ...,
        description="Gear type identifier",
    )
    created_at: datetime_type | None = Field(
        default=None,
        description="Gear creation timestamp",
    )
    active: StrictBool | None = Field(
        default=None,
        description="Whether the gear is active",
    )
    initial_kms: StrictFloat | None = Field(
        default=None,
        ge=0,
        description="Initial kilometers",
    )
    purchase_value: StrictFloat | None = Field(
        default=None,
        ge=0,
        description="Purchase value",
    )
    strava_gear_id: StrictStr | None = Field(
        default=None,
        max_length=45,
        description="Strava gear ID",
    )
    garminconnect_gear_id: StrictStr | None = Field(
        default=None,
        max_length=45,
        description="Garmin Connect gear ID",
    )

    model_config = ConfigDict(
        from_attributes=True,
        extra="forbid",
        validate_assignment=True,
    )

GearCreate

Bases: GearBase

Schema for creating a gear record.

Attributes:

Name Type Description
user_id StrictInt | None

Foreign key to user.

Source code in backend/app/modules/gears/gear/schema.py
114
115
116
117
118
119
120
121
122
123
124
125
class GearCreate(GearBase):
    """
    Schema for creating a gear record.

    Attributes:
        user_id: Foreign key to user.
    """

    user_id: StrictInt | None = Field(
        default=None,
        description="Foreign key to user",
    )

GearRead

Bases: GearBase

Schema for reading a gear record.

Attributes:

Name Type Description
id StrictInt

Unique identifier.

user_id StrictInt

Foreign key to user.

Source code in backend/app/modules/gears/gear/schema.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class GearRead(GearBase):
    """
    Schema for reading a gear record.

    Attributes:
        id: Unique identifier.
        user_id: Foreign key to user.
    """

    id: StrictInt = Field(
        ...,
        description="Unique identifier",
    )
    user_id: StrictInt = Field(
        ...,
        description="Foreign key to user",
    )

GearType

Bases: IntEnum

Supported gear type codes.

Attributes:

Name Type Description
BIKE

Bicycle (1).

SHOES

Running or walking shoes (2).

WETSUIT

Wetsuit (3).

RACQUET

Racquet (4).

SKI

Skis (5).

SNOWBOARD

Snowboard (6).

WINDSURF

Windsurf board (7).

WATER_SPORTS_BOARD

Water sports board (8).

Source code in backend/app/modules/gears/gear/schema.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class GearType(IntEnum):
    """
    Supported gear type codes.

    Attributes:
        BIKE: Bicycle (1).
        SHOES: Running or walking shoes (2).
        WETSUIT: Wetsuit (3).
        RACQUET: Racquet (4).
        SKI: Skis (5).
        SNOWBOARD: Snowboard (6).
        WINDSURF: Windsurf board (7).
        WATER_SPORTS_BOARD: Water sports board (8).
    """

    BIKE = 1
    SHOES = 2
    WETSUIT = 3
    RACQUET = 4
    SKI = 5
    SNOWBOARD = 6
    WINDSURF = 7
    WATER_SPORTS_BOARD = 8

GearUpdate

Bases: GearBase

Schema for updating a gear record.

Attributes:

Name Type Description
id StrictInt

Unique identifier.

Source code in backend/app/modules/gears/gear/schema.py
174
175
176
177
178
179
180
181
182
183
184
185
class GearUpdate(GearBase):
    """
    Schema for updating a gear record.

    Attributes:
        id: Unique identifier.
    """

    id: StrictInt = Field(
        ...,
        description="Unique identifier",
    )

GearsListResponse

Bases: BaseModel

Response model for paginated gear listing.

Attributes:

Name Type Description
total StrictInt

Total number of gear records.

num_records StrictInt | None

Number of records returned.

page_number StrictInt | None

Current page number.

records list[GearRead]

List of gear records.

Source code in backend/app/modules/gears/gear/schema.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
class GearsListResponse(BaseModel):
    """
    Response model for paginated gear listing.

    Attributes:
        total: Total number of gear records.
        num_records: Number of records returned.
        page_number: Current page number.
        records: List of gear records.
    """

    total: StrictInt = Field(
        ...,
        ge=0,
        description="Total number of gear records",
    )
    num_records: StrictInt | None = Field(
        default=None,
        ge=0,
        description="Number of records returned",
    )
    page_number: StrictInt | None = Field(
        default=None,
        ge=1,
        description="Current page number",
    )
    records: list[GearRead] = Field(
        ...,
        description="List of gear records",
    )

    model_config = ConfigDict(
        from_attributes=True,
        extra="forbid",
        validate_assignment=True,
    )

create_gear

create_gear(gear, user_id, db)

Create a single gear for a user.

Parameters:

Name Type Description Default
gear GearCreate

Gear schema with data to create.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
GearRead

Created GearRead schema.

Raises:

Type Description
HTTPException

If gear already exists or a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
@core_decorators.handle_db_errors
def create_gear(
    gear: gears_schema.GearCreate,
    user_id: int,
    db: Session,
) -> gears_schema.GearRead:
    """
    Create a single gear for a user.

    Args:
        gear: Gear schema with data to create.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        Created GearRead schema.

    Raises:
        HTTPException: If gear already exists or
            a database error occurs.
    """
    gear_check = get_gear_user_by_nickname(
        user_id,
        gear.nickname,
        db,
    )

    if gear_check is not None:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=(f"Gear with nickname {gear.nickname} already exists for user {user_id}"),
        )

    gear_data = _normalize_gear_payload(gear.model_dump(exclude_unset=True))
    new_gear = gears_models.Gear(**{**gear_data, "user_id": user_id})

    try:
        db.add(new_gear)
        db.commit()
        db.refresh(new_gear)
    except IntegrityError as integrity_err:
        db.rollback()
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail=("Duplicate entry error. Check if strava_gear_id or garminconnect_gear_id are unique"),
        ) from integrity_err

    return _transform_gears(new_gear)

create_multiple_gears

create_multiple_gears(gears, user_id, db)

Create multiple gears for a user.

Filters invalid entries, deduplicates by nickname, skips existing gears, and persists the rest.

Parameters:

Name Type Description Default
gears list[GearCreate]

List of gear schemas to create.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
None

None.

Raises:

Type Description
HTTPException

If a database or integrity error occurs.

Source code in backend/app/modules/gears/gear/crud.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
@core_decorators.handle_db_errors
def create_multiple_gears(
    gears: list[gears_schema.GearCreate],
    user_id: int,
    db: Session,
) -> None:
    """
    Create multiple gears for a user.

    Filters invalid entries, deduplicates by
    nickname, skips existing gears, and persists
    the rest.

    Args:
        gears: List of gear schemas to create.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        None.

    Raises:
        HTTPException: If a database or
            integrity error occurs.
    """
    # Filter out None and gears without a nickname
    valid_gears = [
        gear
        for gear in (gears or [])
        if gear is not None and getattr(gear, "nickname", None) and str(gear.nickname).strip()
    ]

    # De-dupe by nickname (case-insensitive)
    seen: set[str] = set()
    deduped: list[gears_schema.GearCreate] = []
    for gear in valid_gears:
        nickname_normalized = str(gear.nickname).lower().strip()
        if nickname_normalized not in seen:
            seen.add(nickname_normalized)
            deduped.append(gear)
        else:
            logger.warning(
                f"Duplicate nickname '{gear.nickname}' in request for user {user_id}, skipping",
                extra=core_logger.context(console=True),
            )

    # Fetch all already-existing nicknames for this user in one query
    # instead of one SELECT per gear.
    candidate_nicknames: set[str] = {str(g.nickname).lower().strip() for g in deduped}
    existing_stmt = select(gears_models.Gear.nickname).where(
        gears_models.Gear.user_id == user_id,
        func.lower(gears_models.Gear.nickname).in_(candidate_nicknames),
    )
    existing_nicknames: set[str] = {row[0].lower().strip() for row in db.execute(existing_stmt).all()}

    gears_to_create: list[gears_schema.GearCreate] = []
    for gear in deduped:
        normalised = str(gear.nickname).lower().strip()
        if normalised in existing_nicknames:
            logger.warning(
                f"Gear with nickname '{gear.nickname}' already exists for user {user_id}, skipping",
                extra=core_logger.context(console=True),
            )
        else:
            gears_to_create.append(gear)

    # Persist any remaining
    if gears_to_create:
        new_gears = [
            gears_models.Gear(**{**_normalize_gear_payload(gear.model_dump(exclude_unset=True)), "user_id": user_id})
            for gear in gears_to_create
        ]
        try:
            db.add_all(new_gears)
            db.commit()
            for new_gear in new_gears:
                db.refresh(new_gear)
        except IntegrityError as integrity_err:
            db.rollback()
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail=("Duplicate entry error. Check if strava_gear_id or garminconnect_gear_id are unique"),
            ) from integrity_err

delete_all_garminconnect_gear_for_user

delete_all_garminconnect_gear_for_user(user_id, db)

Delete all Garmin Connect-linked gears for a user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
None

None.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
@core_decorators.handle_db_errors
def delete_all_garminconnect_gear_for_user(user_id: int, db: Session) -> None:
    """
    Delete all Garmin Connect-linked gears for a
    user.

    Args:
        user_id: Owner user ID.
        db: Database session.

    Returns:
        None.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = delete(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
        gears_models.Gear.garminconnect_gear_id.isnot(None),
    )
    db.execute(stmt)
    db.commit()

delete_all_strava_gear_for_user

delete_all_strava_gear_for_user(user_id, db)

Delete all Strava-linked gears for a user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
None

None.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
@core_decorators.handle_db_errors
def delete_all_strava_gear_for_user(user_id: int, db: Session) -> None:
    """
    Delete all Strava-linked gears for a user.

    Args:
        user_id: Owner user ID.
        db: Database session.

    Returns:
        None.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = delete(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
        gears_models.Gear.strava_gear_id.isnot(
            None,
        ),
    )
    db.execute(stmt)
    db.commit()

delete_gear

delete_gear(gear_id, user_id, db)

Delete a gear by ID and user ID.

Parameters:

Name Type Description Default
gear_id int

Gear ID to delete.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
None

None.

Raises:

Type Description
HTTPException

If gear not found or a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
@core_decorators.handle_db_errors
def delete_gear(gear_id: int, user_id: int, db: Session) -> None:
    """
    Delete a gear by ID and user ID.

    Args:
        gear_id: Gear ID to delete.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        None.

    Raises:
        HTTPException: If gear not found or
            a database error occurs.
    """
    db_gear = _get_gear_model_by_id_and_user_id_or_404(
        gear_id,
        user_id,
        db,
    )

    db.delete(db_gear)
    db.commit()

edit_gear

edit_gear(gear, user_id, db)

Edit an existing gear by ID and user ID.

Parameters:

Name Type Description Default
gear GearUpdate

Gear schema with updated fields.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
GearRead

Updated GearRead schema.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
@core_decorators.handle_db_errors
def edit_gear(
    gear: gears_schema.GearUpdate,
    user_id: int,
    db: Session,
) -> gears_schema.GearRead:
    """
    Edit an existing gear by ID and user ID.

    Args:
        gear: Gear schema with updated fields.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        Updated GearRead schema.

    Raises:
        HTTPException: If a database error occurs.
    """
    db_gear = _get_gear_model_by_id_and_user_id_or_404(
        gear.id,
        user_id,
        db,
    )

    gear_data = _normalize_gear_payload(
        gear.model_dump(exclude_unset=True),
    )

    for key, value in gear_data.items():
        if key in _IMMUTABLE_FIELDS or value is None:
            continue
        setattr(db_gear, key, value)

    db.commit()
    db.refresh(db_gear)

    return _transform_gears(db_gear)

get_gear_by_garminconnect_id_from_user_id

get_gear_by_garminconnect_id_from_user_id(gear_garminconnect_id, user_id, db)

Retrieve a gear by Garmin Connect ID for a user.

Parameters:

Name Type Description Default
gear_garminconnect_id str

Garmin Connect gear ID.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
GearRead | None

GearRead schema if found, None otherwise.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
@core_decorators.handle_db_errors
def get_gear_by_garminconnect_id_from_user_id(
    gear_garminconnect_id: str,
    user_id: int,
    db: Session,
) -> gears_schema.GearRead | None:
    """
    Retrieve a gear by Garmin Connect ID for a user.

    Args:
        gear_garminconnect_id: Garmin Connect gear ID.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        GearRead schema if found, None otherwise.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
        gears_models.Gear.garminconnect_gear_id == gear_garminconnect_id,
    )
    db_gear = db.execute(stmt).scalar_one_or_none()
    return _transform_gears(db_gear) if db_gear else None

get_gear_by_strava_id_from_user_id

get_gear_by_strava_id_from_user_id(gear_strava_id, user_id, db)

Retrieve a gear by Strava ID for a user.

Parameters:

Name Type Description Default
gear_strava_id str

Strava gear identifier.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
GearRead | None

GearRead schema if found, None otherwise.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
@core_decorators.handle_db_errors
def get_gear_by_strava_id_from_user_id(
    gear_strava_id: str,
    user_id: int,
    db: Session,
) -> gears_schema.GearRead | None:
    """
    Retrieve a gear by Strava ID for a user.

    Args:
        gear_strava_id: Strava gear identifier.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        GearRead schema if found, None otherwise.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
        gears_models.Gear.strava_gear_id == gear_strava_id,
    )
    db_gear = db.execute(stmt).scalar_one_or_none()
    return _transform_gears(db_gear) if db_gear else None

get_gear_by_type_and_user

get_gear_by_type_and_user(gear_type, user_id, db)

Retrieve gears by type for a specific user.

Parameters:

Name Type Description Default
gear_type int

Gear type identifier.

required
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
list[GearRead]

List of GearRead schemas ordered by nickname.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
@core_decorators.handle_db_errors
def get_gear_by_type_and_user(gear_type: int, user_id: int, db: Session) -> list[gears_schema.GearRead]:
    """
    Retrieve gears by type for a specific user.

    Args:
        gear_type: Gear type identifier.
        user_id: Owner user ID.
        db: Database session.

    Returns:
        List of GearRead schemas ordered by nickname.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = (
        select(gears_models.Gear)
        .where(
            gears_models.Gear.gear_type == gear_type,
            gears_models.Gear.user_id == user_id,
        )
        .order_by(gears_models.Gear.nickname)
    )
    db_gears = db.execute(stmt).scalars().all()
    return _transform_gears(list(db_gears))

get_gear_user

get_gear_user(user_id, db)

Retrieve all gears for a specific user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
list[GearRead]

List of GearRead schemas for the user.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
@core_decorators.handle_db_errors
def get_gear_user(user_id: int, db: Session) -> list[gears_schema.GearRead]:
    """
    Retrieve all gears for a specific user.

    Args:
        user_id: Owner user ID.
        db: Database session.

    Returns:
        List of GearRead schemas for the user.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
    )
    db_gears = db.execute(stmt).scalars().all()
    return _transform_gears(list(db_gears))

get_gear_user_by_id

get_gear_user_by_id(user_id, gear_id, db)

Retrieve a gear by ID for a specific user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
gear_id int

Gear ID to fetch.

required
db Session

Database session.

required

Returns:

Type Description
GearRead | None

GearRead schema if found, None otherwise.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@core_decorators.handle_db_errors
def get_gear_user_by_id(user_id: int, gear_id: int, db: Session) -> gears_schema.GearRead | None:
    """
    Retrieve a gear by ID for a specific user.

    Args:
        user_id: Owner user ID.
        gear_id: Gear ID to fetch.
        db: Database session.

    Returns:
        GearRead schema if found, None otherwise.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
        gears_models.Gear.id == gear_id,
    )
    db_gear = db.execute(stmt).scalar_one_or_none()
    return _transform_gears(db_gear) if db_gear is not None else None

get_gear_user_by_nickname

get_gear_user_by_nickname(user_id, nickname, db)

Retrieve a gear by exact nickname for a user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
nickname str

Gear nickname to match.

required
db Session

Database session.

required

Returns:

Type Description
GearRead | None

GearRead schema if found, None otherwise.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
@core_decorators.handle_db_errors
def get_gear_user_by_nickname(user_id: int, nickname: str, db: Session) -> gears_schema.GearRead | None:
    """
    Retrieve a gear by exact nickname for a user.

    Args:
        user_id: Owner user ID.
        nickname: Gear nickname to match.
        db: Database session.

    Returns:
        GearRead schema if found, None otherwise.

    Raises:
        HTTPException: If a database error occurs.
    """
    parsed_nickname = nickname.lower().strip()

    stmt = select(gears_models.Gear).where(
        func.lower(
            gears_models.Gear.nickname,
        )
        == parsed_nickname,
        gears_models.Gear.user_id == user_id,
    )
    db_gear = db.execute(stmt).scalar_one_or_none()
    return _transform_gears(db_gear) if db_gear else None

get_gear_user_contains_nickname

get_gear_user_contains_nickname(user_id, nickname, db)

Retrieve gears matching a nickname substring.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
nickname str

Substring to search for.

required
db Session

Database session.

required

Returns:

Type Description
list[GearRead]

List of GearRead schemas matching criteria.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@core_decorators.handle_db_errors
def get_gear_user_contains_nickname(user_id: int, nickname: str, db: Session) -> list[gears_schema.GearRead]:
    """
    Retrieve gears matching a nickname substring.

    Args:
        user_id: Owner user ID.
        nickname: Substring to search for.
        db: Database session.

    Returns:
        List of GearRead schemas matching criteria.

    Raises:
        HTTPException: If a database error occurs.
    """
    parsed_nickname = nickname.lower().strip()

    stmt = select(gears_models.Gear).where(
        func.lower(
            gears_models.Gear.nickname,
        ).like(f"%{parsed_nickname}%"),
        gears_models.Gear.user_id == user_id,
    )
    db_gears = db.execute(stmt).scalars().all()
    return _transform_gears(list(db_gears))

get_gear_users_with_pagination

get_gear_users_with_pagination(user_id, db, page_number=None, num_records=None, show_inactive=True)

Retrieve paginated gears for a user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
db Session

Database session.

required
page_number int | None

Page number (1-indexed). Defaults to None.

None
num_records int | None

Records per page. Defaults to None.

None
show_inactive bool | None

Include inactive gears. Defaults to True.

True

Returns:

Type Description
list[GearRead]

List of GearRead schemas ordered by nickname.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
@core_decorators.handle_db_errors
def get_gear_users_with_pagination(
    user_id: int,
    db: Session,
    page_number: int | None = None,
    num_records: int | None = None,
    show_inactive: bool | None = True,
) -> list[gears_schema.GearRead]:
    """
    Retrieve paginated gears for a user.

    Args:
        user_id: Owner user ID.
        db: Database session.
        page_number: Page number (1-indexed). Defaults to None.
        num_records: Records per page. Defaults to None.
        show_inactive: Include inactive gears. Defaults to True.

    Returns:
        List of GearRead schemas ordered by nickname.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(gears_models.Gear).where(
        gears_models.Gear.user_id == user_id,
    )

    if show_inactive is False:
        stmt = stmt.where(
            gears_models.Gear.active.is_(True),
        )

    stmt = stmt.order_by(gears_models.Gear.nickname)

    if page_number is not None and num_records is not None:
        stmt = stmt.offset(
            (page_number - 1) * num_records,
        ).limit(num_records)

    db_gears = db.execute(stmt).scalars().all()
    return _transform_gears(list(db_gears))

get_gears_number

get_gears_number(user_id, db, show_inactive=True)

Count a user's gears, honoring the active filter.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
db Session

Database session.

required
show_inactive bool | None

Include inactive gears. Defaults to True.

True

Returns:

Type Description
int

Number of gears matching the filter.

Raises:

Type Description
HTTPException

If a database error occurs.

Source code in backend/app/modules/gears/gear/crud.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
@core_decorators.handle_db_errors
def get_gears_number(
    user_id: int,
    db: Session,
    show_inactive: bool | None = True,
) -> int:
    """
    Count a user's gears, honoring the active filter.

    Args:
        user_id: Owner user ID.
        db: Database session.
        show_inactive: Include inactive gears. Defaults to True.

    Returns:
        Number of gears matching the filter.

    Raises:
        HTTPException: If a database error occurs.
    """
    stmt = select(func.count(gears_models.Gear.id)).where(
        gears_models.Gear.user_id == user_id,
    )

    if show_inactive is False:
        stmt = stmt.where(
            gears_models.Gear.active.is_(True),
        )

    return db.execute(stmt).scalar_one()

Gear components

Gear components sub-module.

Provides CRUD operations, schemas, and models for gear component tracking (e.g., chains, tires, pedals, strings).

Exports
  • CRUD: get_gear_components_user, get_gear_components_user_by_gear_id, create_gear_component, edit_gear_component, delete_gear_component
  • Schemas: GearComponentBase, GearComponentCreate, GearComponentRead, GearComponentUpdate, GearComponentTypesRead
  • Models: GearComponents (ORM model)

GearComponentBase

Bases: BaseModel

Base model for gear component data.

Attributes:

Name Type Description
gear_id StrictInt

Foreign key to gear table.

type StrictStr

Type of gear component.

brand StrictStr

Gear component brand.

model StrictStr

Gear component model name.

purchase_date date | None

Purchase date (calendar date, no time of day).

retired_date date | None

Retired date (calendar date, no time of day).

active StrictBool | None

Whether component is active.

expected_kms StrictInt | None

Expected kilometers.

purchase_value StrictFloat | None

Purchase value.

Source code in backend/app/modules/gears/gear_components/schema.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class GearComponentBase(BaseModel):
    """
    Base model for gear component data.

    Attributes:
        gear_id: Foreign key to gear table.
        type: Type of gear component.
        brand: Gear component brand.
        model: Gear component model name.
        purchase_date: Purchase date (calendar date, no time of day).
        retired_date: Retired date (calendar date, no time of day).
        active: Whether component is active.
        expected_kms: Expected kilometers.
        purchase_value: Purchase value.
    """

    gear_id: StrictInt = Field(
        ...,
        description="Foreign key to gear",
    )
    type: StrictStr = Field(
        ...,
        max_length=250,
        description="Type of gear component",
    )
    brand: StrictStr = Field(
        ...,
        max_length=250,
        description="Gear component brand",
    )
    model: StrictStr = Field(
        ...,
        max_length=250,
        description="Gear component model",
    )
    purchase_date: date | None = Field(
        default=None,
        description="Purchase date",
    )
    retired_date: date | None = Field(
        default=None,
        description="Retired date",
    )
    active: StrictBool | None = Field(
        default=None,
        description=("Whether the component is active"),
    )
    expected_kms: StrictInt | None = Field(
        default=None,
        ge=0,
        description=("Expected kilometers"),
    )
    purchase_value: StrictFloat | None = Field(
        default=None,
        ge=0,
        description="Purchase value",
    )

    model_config = ConfigDict(
        from_attributes=True,
        extra="forbid",
        validate_assignment=True,
    )

GearComponentCreate

Bases: GearComponentBase

Schema for creating a gear component.

Source code in backend/app/modules/gears/gear_components/schema.py
164
165
class GearComponentCreate(GearComponentBase):
    """Schema for creating a gear component."""

GearComponentRead

Bases: GearComponentBase

Schema for reading a gear component.

Attributes:

Name Type Description
id StrictInt

Unique identifier.

user_id StrictInt

Foreign key to user.

current_distance StrictFloat

Accumulated distance.

current_time StrictFloat

Accumulated time.

Source code in backend/app/modules/gears/gear_components/schema.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
class GearComponentRead(GearComponentBase):
    """
    Schema for reading a gear component.

    Attributes:
        id: Unique identifier.
        user_id: Foreign key to user.
        current_distance: Accumulated distance.
        current_time: Accumulated time.
    """

    id: StrictInt = Field(
        ...,
        description="Unique identifier",
    )
    user_id: StrictInt = Field(
        ...,
        description="Foreign key to user",
    )
    current_distance: StrictFloat = Field(
        default=0,
        ge=0,
        description=("Accumulated activity distance in meters"),
    )
    current_time: StrictFloat = Field(
        default=0,
        ge=0,
        description=("Accumulated activity time in seconds"),
    )

GearComponentTypesRead

Bases: BaseModel

Schema for reading component type lists.

Attributes:

Name Type Description
bike list[str]

Valid bike component types.

shoes list[str]

Valid shoes component types.

racquet list[str]

Valid racquet component types.

windsurf list[str]

Valid windsurf component types.

Source code in backend/app/modules/gears/gear_components/schema.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
class GearComponentTypesRead(BaseModel):
    """
    Schema for reading component type lists.

    Attributes:
        bike: Valid bike component types.
        shoes: Valid shoes component types.
        racquet: Valid racquet component types.
        windsurf: Valid windsurf component types.
    """

    bike: list[str] = Field(
        ...,
        description=("Valid bike component types"),
    )
    shoes: list[str] = Field(
        ...,
        description=("Valid shoes component types"),
    )
    racquet: list[str] = Field(
        ...,
        description=("Valid racquet component types"),
    )
    windsurf: list[str] = Field(
        ...,
        description=("Valid windsurf component types"),
    )

    model_config = ConfigDict(
        extra="forbid",
    )

GearComponentUpdate

Bases: GearComponentBase

Schema for updating a gear component.

Attributes:

Name Type Description
id StrictInt

Unique identifier.

Source code in backend/app/modules/gears/gear_components/schema.py
199
200
201
202
203
204
205
206
207
208
209
210
class GearComponentUpdate(GearComponentBase):
    """
    Schema for updating a gear component.

    Attributes:
        id: Unique identifier.
    """

    id: StrictInt = Field(
        ...,
        description="Unique identifier",
    )

create_gear_component

create_gear_component(gear_component, user_id, db)

Create a new gear component.

Parameters:

Name Type Description Default
gear_component GearComponentCreate

Create schema data.

required
user_id int

Authenticated user ID (token).

required
db Session

Database session.

required

Returns:

Type Description
GearComponentRead

Created GearComponentRead schema.

Raises:

Type Description
HTTPException

On database error (500).

Source code in backend/app/modules/gears/gear_components/crud.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
@core_decorators.handle_db_errors
def create_gear_component(
    gear_component: (gear_components_schema.GearComponentCreate),
    user_id: int,
    db: Session,
) -> gear_components_schema.GearComponentRead:
    """
    Create a new gear component.

    Args:
        gear_component: Create schema data.
        user_id: Authenticated user ID (token).
        db: Database session.

    Returns:
        Created GearComponentRead schema.

    Raises:
        HTTPException: On database error (500).
    """
    new_gear_component = gear_components_models.GearComponents(
        user_id=user_id,
        gear_id=gear_component.gear_id,
        type=gear_component.type,
        brand=gear_component.brand,
        model=gear_component.model,
        purchase_date=(gear_component.purchase_date),
        active=True,
        expected_kms=(gear_component.expected_kms),
        purchase_value=(gear_component.purchase_value),
    )

    db.add(new_gear_component)
    db.commit()
    db.refresh(new_gear_component)

    return _transform_gear_components(new_gear_component)

delete_gear_component

delete_gear_component(user_id, gear_component_id, db)

Delete a gear component by user and ID.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
gear_component_id int

Component ID to delete.

required
db Session

Database session.

required

Returns:

Type Description
None

None.

Raises:

Type Description
HTTPException

If not found (404) or database error (500).

Source code in backend/app/modules/gears/gear_components/crud.py
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
@core_decorators.handle_db_errors
def delete_gear_component(
    user_id: int,
    gear_component_id: int,
    db: Session,
) -> None:
    """
    Delete a gear component by user and ID.

    Args:
        user_id: Owner user ID.
        gear_component_id: Component ID to delete.
        db: Database session.

    Returns:
        None.

    Raises:
        HTTPException: If not found (404) or
            database error (500).
    """
    db_gear_component = _get_gear_component_model_by_id_and_user_id_or_404(gear_component_id, user_id, db)

    db.delete(db_gear_component)
    db.commit()

edit_gear_component

edit_gear_component(gear_component, user_id, db)

Edit an existing gear component by ID.

Applies a partial update (only fields present in the payload are changed) and enforces the retired/active invariant: a retired component (retired_date set) is always inactive, while an un-retired component keeps the client-supplied active value.

Parameters:

Name Type Description Default
gear_component GearComponentUpdate

Update schema data.

required
db Session

Database session.

required

Returns:

Type Description
GearComponentRead

Updated GearComponentRead schema.

Raises:

Type Description
HTTPException

If not found (404) or database error (500).

Source code in backend/app/modules/gears/gear_components/crud.py
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
@core_decorators.handle_db_errors
def edit_gear_component(
    gear_component: (gear_components_schema.GearComponentUpdate),
    user_id: int,
    db: Session,
) -> gear_components_schema.GearComponentRead:
    """
    Edit an existing gear component by ID.

    Applies a partial update (only fields present in the payload are
    changed) and enforces the retired/active invariant: a retired
    component (``retired_date`` set) is always inactive, while an
    un-retired component keeps the client-supplied ``active`` value.

    Args:
        gear_component: Update schema data.
        db: Database session.

    Returns:
        Updated GearComponentRead schema.

    Raises:
        HTTPException: If not found (404) or
            database error (500).
    """
    db_gear_component = _get_gear_component_model_by_id_and_user_id_or_404(gear_component.id, user_id, db)

    gear_component_data = gear_component.model_dump(
        exclude_unset=True,
    )
    for key, value in gear_component_data.items():
        if key not in _IMMUTABLE_FIELDS:
            setattr(db_gear_component, key, value)

    # Enforce the retired/active invariant: a retired component is always
    # inactive. When the component is not retired, the client-supplied
    # ``active`` value (already applied above) is honoured as-is. The
    # server never infers reactivation from field presence, so behaviour
    # is deterministic across every client; reactivating a retired
    # component is an explicit client action (clear ``retired_date`` and
    # set ``active`` to True in the same request).
    if db_gear_component.retired_date is not None:
        db_gear_component.active = False

    db.commit()
    db.refresh(db_gear_component)

    return _transform_gear_components(db_gear_component)

get_gear_components_user

get_gear_components_user(user_id, db)

Retrieve all gear components for a user.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
db Session

Database session.

required

Returns:

Type Description
list[GearComponentRead]

List of GearComponentRead schema.

Raises:

Type Description
HTTPException

On database error (500).

Source code in backend/app/modules/gears/gear_components/crud.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@core_decorators.handle_db_errors
def get_gear_components_user(
    user_id: int,
    db: Session,
) -> list[gear_components_schema.GearComponentRead]:
    """
    Retrieve all gear components for a user.

    Args:
        user_id: Owner user ID.
        db: Database session.

    Returns:
        List of GearComponentRead schema.

    Raises:
        HTTPException: On database error (500).
    """
    stmt = select(
        gear_components_models.GearComponents,
    ).where(
        gear_components_models.GearComponents.user_id == user_id,
    )
    db_gear_components = db.execute(stmt).scalars().all()
    return _transform_gear_components(list(db_gear_components))

get_gear_components_user_by_gear_id

get_gear_components_user_by_gear_id(user_id, gear_id, db, active=None)

Retrieve gear components by user and gear.

Parameters:

Name Type Description Default
user_id int

Owner user ID.

required
gear_id int

Gear ID to filter by.

required
db Session

Database session.

required
active bool | None

Optional active-status filter.

None

Returns:

Type Description
list[GearComponentRead]

List of GearComponentRead schema.

Raises:

Type Description
HTTPException

On database error (500).

Source code in backend/app/modules/gears/gear_components/crud.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
@core_decorators.handle_db_errors
def get_gear_components_user_by_gear_id(
    user_id: int,
    gear_id: int,
    db: Session,
    active: bool | None = None,
) -> list[gear_components_schema.GearComponentRead]:
    """
    Retrieve gear components by user and gear.

    Args:
        user_id: Owner user ID.
        gear_id: Gear ID to filter by.
        db: Database session.
        active: Optional active-status filter.

    Returns:
        List of GearComponentRead schema.

    Raises:
        HTTPException: On database error (500).
    """
    stmt = select(
        gear_components_models.GearComponents,
    ).where(
        gear_components_models.GearComponents.user_id == user_id,
        gear_components_models.GearComponents.gear_id == gear_id,
    )
    if active is not None:
        stmt = stmt.where(
            gear_components_models.GearComponents.active == active,
        )
    db_gear_components = db.execute(stmt).scalars().all()
    return _transform_gear_components(list(db_gear_components))