Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,5 @@ cython_debug/
.cursorignore
.cursorindexingignore

alembic/versions/
alembic/versions/
.DS_Store
20 changes: 16 additions & 4 deletions config/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy.orm import sessionmaker, DeclarativeBase
import os
from dotenv import load_dotenv
import warnings
Expand All @@ -13,8 +13,19 @@
# Read the database URL from the environment variable
DATABASE_URL = os.getenv("DATABASE_URL")
if DATABASE_URL is not None:
# Create the SQLAlchemy engine with the database URL
engine = create_engine(DATABASE_URL)
# Create the SQLAlchemy engine with the database URL.
# pool_size / max_overflow cap concurrent DB connections so one slow
# query can't starve the whole server. statement_timeout kills any
# single query that runs longer than 60 s so a runaway request doesn't
# hold a connection indefinitely.
engine = create_engine(
DATABASE_URL,
pool_size=10,
max_overflow=5,
pool_timeout=30,
pool_pre_ping=True,
connect_args={"options": "-c statement_timeout=60000"},
)
# Create a session factory
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
else:
Expand All @@ -23,7 +34,8 @@
# Base is the base class for all the SQLAlchemy ORM models.
# It tells SQLAlchemy that a model maps to a real table.
# Without inheriting from Base, the class won’t be recognized by SQLAlchemy’s ORM.
Base = declarative_base()
class Base(DeclarativeBase):
pass

# Dependency to get a DB session for FastAPI routes (used in controllers)
def get_db():
Expand Down
13 changes: 11 additions & 2 deletions controllers/disco_controller.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, Query, Request
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.orm import Session
from services.disco_service import DiscoService
from dtos.generic_response_dto import GenericResponseDTO, build_url
Expand Down Expand Up @@ -56,16 +56,24 @@ async def get_events(
None, description="Total number of Atlas probes active in the reported stream (ASN, Country, or geographical area)."),
ongoing: Optional[str] = Query(
None, description="Deprecated, this value is unused"),
include_probe_details: bool = Query(
False, description="Include per-probe details in the response."),
page: Optional[int] = Query(
1, ge=1, description="A page number within the paginated result set."),
ordering: Optional[str] = Query(
None, description="Which field to use when ordering the results")
) -> GenericResponseDTO[DiscoEventsDTO]:
"""
List network disconnections detected with RIPE Atlas.
List network disconnections detected with RIPE Atlas.
These events have different levels of granularity - it can be at a network level (AS), city, or country level.
"""

if not any([starttime, starttime__gte, starttime__lte, endtime, endtime__gte, endtime__lte]):
raise HTTPException(
status_code=400,
detail="At least one time parameter is required: starttime, starttime__gte, starttime__lte, endtime, endtime__gte, or endtime__lte."
)

events_data, total_count = DiscoController.service.get_disco_events(
db,
streamname=streamname,
Expand All @@ -86,6 +94,7 @@ async def get_events(
totalprobes_gte=totalprobes__gte,
totalprobes_lte=totalprobes__lte,
ongoing=ongoing,
include_probe_details=include_probe_details,
page=page,
order_by=ordering
)
Expand Down
54 changes: 51 additions & 3 deletions docs/add_new_endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,55 @@ Create a service file in the `services/` directory or modify an existing one. Th
### 3. **Create the Repository**
Add a repository file in the `repositories/` directory or modify an existing one. Ensure it handles pagination and ordering using `offset` and `limit`.

Use the SQLAlchemy 2.0 `select()` style — **not** the legacy `db.query()` API.

Example:
```python
# filepath: repositories/new_entity_repository.py
from sqlalchemy.orm import Session
from sqlalchemy import select, func
from models.new_entity_model import NewEntity
from typing import Optional, List, Tuple
from utils import page_size


class NewEntityRepository:
def get_all(
self,
db: Session,
field1: Optional[str] = None,
page: int = 1,
order_by: Optional[str] = None,
) -> Tuple[List[NewEntity], int]:
stmt = select(NewEntity)

if field1:
stmt = stmt.where(NewEntity.field1 == field1)

total_count = db.scalar(select(func.count()).select_from(stmt.subquery()))

if order_by and hasattr(NewEntity, order_by):
stmt = stmt.order_by(getattr(NewEntity, order_by))

offset = (page - 1) * page_size
results = db.scalars(stmt.offset(offset).limit(page_size)).all()

return results, total_count
```

If the model has a relationship that needs to be loaded eagerly alongside the main query, use `contains_eager` with `of_type()`:
```python
from sqlalchemy.orm import contains_eager, aliased

RelatedModel = aliased(NewEntity.related_relation.property.mapper.class_)
stmt = (
select(NewEntity)
.join(NewEntity.related_relation.of_type(RelatedModel))
.options(contains_eager(NewEntity.related_relation.of_type(RelatedModel)))
)
# then add .where() clauses and call db.scalars(...).unique().all()
```

---

### 4. **Define the Model**
Expand Down Expand Up @@ -88,14 +137,13 @@ Add a DTO in the `dtos/` directory to define the structure of the response.
Example:
```python
# filepath: dtos/new_entity_dto.py
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

class NewEntityDTO(BaseModel):
field1: str
field2: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
```
---

Expand Down
5 changes: 2 additions & 3 deletions dtos/country_dto.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

class CountryDTO(BaseModel):
code: str
name: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
13 changes: 6 additions & 7 deletions dtos/disco_events_dto.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from dtos.disco_probes_dto import DiscoProbesDTO
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from typing import List, Optional

Expand All @@ -13,13 +13,12 @@ class DiscoEventsDTO(BaseModel):
nbdiscoprobes: int
totalprobes: int
ongoing: bool
discoprobes: List[DiscoProbesDTO]
discoprobes: Optional[List[DiscoProbesDTO]] = None

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)

@staticmethod
def from_model(disco_event):
def from_model(disco_event, include_probe_details: bool = False):
return DiscoEventsDTO(
id=disco_event.id,
streamtype=disco_event.streamtype,
Expand All @@ -30,6 +29,6 @@ def from_model(disco_event):
nbdiscoprobes=disco_event.nbdiscoprobes,
totalprobes=disco_event.totalprobes,
ongoing=disco_event.ongoing,
discoprobes=[DiscoProbesDTO.from_orm(
probe) for probe in disco_event.probes]
discoprobes=[DiscoProbesDTO.model_validate(probe) for probe in disco_event.probes]
if include_probe_details else None,
)
5 changes: 2 additions & 3 deletions dtos/disco_probes_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -13,5 +13,4 @@ class DiscoProbesDTO(BaseModel):
lat: float
lon: float

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/hegemony_alarms_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -11,5 +11,4 @@ class HegemonyAlarmsDTO(BaseModel):
asn_name: str
originasn_name: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/hegemony_cone_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -8,5 +8,4 @@ class HegemonyConeDTO(BaseModel):
conesize: int
af: int

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/hegemony_country_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -13,5 +13,4 @@ class HegemonyCountryDTO(BaseModel):
weightscheme: str
transitonly: bool

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/hegemony_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -11,5 +11,4 @@ class HegemonyDTO(BaseModel):
asn_name: str
originasn_name: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/hegemony_prefix_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -20,5 +20,4 @@ class HegemonyPrefixDTO(BaseModel):
originasn_name: str
asn_name: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/metis_atlas_deployment_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -11,5 +11,4 @@ class MetisAtlasDeploymentDTO(BaseModel):
nbsamples: int
asn_name: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/metis_atlas_selection_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -10,5 +10,4 @@ class MetisAtlasSelectionDTO(BaseModel):
af: int
asn_name: str

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/network_delay_alarms_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -12,8 +12,7 @@ class NetworkDelayAlarmsDTO(BaseModel):
endpoint_af: int
deviation: float

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)

@staticmethod
def from_model(atlas_delay_alarm):
Expand Down
5 changes: 2 additions & 3 deletions dtos/network_delay_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -17,8 +17,7 @@ class NetworkDelayDTO(BaseModel):
hop: int
nbrealrtts: int

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)

@staticmethod
def from_model(atlasDelay):
Expand Down
5 changes: 2 additions & 3 deletions dtos/network_delay_locations_dto.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict


class NetworkDelayLocationsDTO(BaseModel):
type: str
name: str
af: int

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
5 changes: 2 additions & 3 deletions dtos/networks_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict


class NetworksDTO(BaseModel):
Expand All @@ -8,8 +8,7 @@ class NetworksDTO(BaseModel):
delay_forwarding: bool
disco: bool

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)

@staticmethod
def from_model(asn):
Expand Down
5 changes: 2 additions & 3 deletions dtos/tr_hegemony_dto.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict
from datetime import datetime


Expand All @@ -14,8 +14,7 @@ class TRHegemonyDTO(BaseModel):
af: int
nbsamples: int

class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)

@staticmethod
def from_model(tr_hegemony):
Expand Down
Loading
Loading