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
30 changes: 30 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Lint & Format

on:
pull_request:
branches:
- main

jobs:
check-code:
runs-on: ubuntu-latest

steps:
- name: Checkout repo
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff black

- name: Run black
run: black --check .

- name: Run ruff
run: ruff check .
3 changes: 1 addition & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ help: ##@Help Show this help
lint: ##@Code Check code with ruff
poetry run ruff check . --fix

format: ##@Code Reformat code with ruff, isort, black
format: ##@Code Reformat code with isort, black
poetry run isort .
poetry run black .
poetry run ruff check --fix .

db: ##@Project Run server and database with docker-compose
docker-compose -f docker-compose.yml up -d --remove-orphans
Expand Down
2 changes: 1 addition & 1 deletion app/db/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio

from logging.config import fileConfig

from alembic import context
Expand All @@ -9,7 +10,6 @@
from app.config import get_settings
from app.db.models import weather


# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
Expand Down
53 changes: 17 additions & 36 deletions app/db/alembic/versions/2025_05_21_1833-6b01625ca9a1_.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
"""empty message

Revision ID: 6b01625ca9a1
Revises:
Revises:
Create Date: 2025-05-21 18:33:41.409451

"""

from typing import Sequence, Union
from collections.abc import Sequence

from alembic import op
import sqlalchemy as sa

from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision: str = '6b01625ca9a1'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
Expand All @@ -30,15 +31,9 @@ def upgrade() -> None:
server_default=sa.text('now()'),
nullable=False,
),
sa.Column(
'temperature', sa.NUMERIC(precision=4, scale=1), nullable=False
),
sa.Column(
'humidity', sa.NUMERIC(precision=4, scale=1), nullable=False
),
sa.Column(
'pressure', sa.NUMERIC(precision=6, scale=1), nullable=False
),
sa.Column('temperature', sa.NUMERIC(precision=4, scale=1), nullable=False),
sa.Column('humidity', sa.NUMERIC(precision=4, scale=1), nullable=False),
sa.Column('pressure', sa.NUMERIC(precision=6, scale=1), nullable=False),
sa.Column('co2', sa.INTEGER(), nullable=False),
sa.Column('tvoc', sa.INTEGER(), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk__central')),
Expand All @@ -52,23 +47,15 @@ def upgrade() -> None:
server_default=sa.text('now()'),
nullable=False,
),
sa.Column(
'weather_description', sa.VARCHAR(length=255), nullable=False
),
sa.Column('weather_description', sa.VARCHAR(length=255), nullable=False),
sa.Column(
'temperature_feels_like',
sa.NUMERIC(precision=4, scale=1),
nullable=False,
),
sa.Column(
'precipitation', sa.NUMERIC(precision=4, scale=1), nullable=False
),
sa.Column(
'uv_index', sa.NUMERIC(precision=3, scale=1), nullable=False
),
sa.Column(
'wind_speed', sa.NUMERIC(precision=4, scale=1), nullable=False
),
sa.Column('precipitation', sa.NUMERIC(precision=4, scale=1), nullable=False),
sa.Column('uv_index', sa.NUMERIC(precision=3, scale=1), nullable=False),
sa.Column('wind_speed', sa.NUMERIC(precision=4, scale=1), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk__external_weather')),
)
op.create_table(
Expand All @@ -80,15 +67,9 @@ def upgrade() -> None:
server_default=sa.text('now()'),
nullable=False,
),
sa.Column(
'temperature', sa.NUMERIC(precision=4, scale=1), nullable=False
),
sa.Column(
'humidity', sa.NUMERIC(precision=4, scale=1), nullable=False
),
sa.Column(
'pressure', sa.NUMERIC(precision=6, scale=1), nullable=False
),
sa.Column('temperature', sa.NUMERIC(precision=4, scale=1), nullable=False),
sa.Column('humidity', sa.NUMERIC(precision=4, scale=1), nullable=False),
sa.Column('pressure', sa.NUMERIC(precision=6, scale=1), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk__outdoor')),
)
# ### end Alembic commands ###
Expand Down
37 changes: 37 additions & 0 deletions app/db/alembic/versions/2025_05_22_0245-6a78a8667cc4_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""empty message

Revision ID: 6a78a8667cc4
Revises: 6b01625ca9a1
Create Date: 2025-05-22 02:45:36.570023

"""

from collections.abc import Sequence

import sqlalchemy as sa

from alembic import op

# revision identifiers, used by Alembic.
revision: str = '6a78a8667cc4'
down_revision: str | None = '6b01625ca9a1'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'settings',
sa.Column('key', sa.VARCHAR(length=255), nullable=False),
sa.Column('value', sa.VARCHAR(length=255), nullable=False),
sa.Column('type', sa.VARCHAR(length=255), nullable=False),
sa.PrimaryKeyConstraint('key', name=op.f('pk__settings')),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('settings')
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions app/db/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from app.db.models.setting import Setting
from app.db.models.weather import Central, ExternalWeather, Outdoor

__all__ = [
"Setting",
"Central",
"Outdoor",
"ExternalWeather",
Expand Down
33 changes: 33 additions & 0 deletions app/db/models/setting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from sqlalchemy import Column
from sqlalchemy.dialects.postgresql import VARCHAR

from app.db import DeclarativeBase


class Setting(DeclarativeBase):
__tablename__ = "settings"

key = Column(
VARCHAR(255),
primary_key=True,
doc="Key of the setting",
)
value = Column(
VARCHAR(255),
nullable=False,
doc="Value of the setting",
)
type = Column(
VARCHAR(255),
nullable=False,
doc="Type of the setting",
)

def __repr__(self):
columns = {
column.name: getattr(self, column.name) for column in self.__table__.columns
}
return (
f'<{self.__tablename__}: '
f'{", ".join(map(lambda x: f"{x[0]}={x[1]}", columns.items()))}>'
)
8 changes: 7 additions & 1 deletion app/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
list_of_routes = []
from app.endpoints.setting import api_router as get_setting_router
from app.endpoints.weather import api_router as get_weather_router

list_of_routes = [
get_weather_router,
get_setting_router,
]


__all__ = [
Expand Down
29 changes: 29 additions & 0 deletions app/endpoints/setting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status

from app.db.connection import get_session
from app.schemas import SettingPatchWithKey
from app.utils.queries import save_multiple_settings

api_router = APIRouter(tags=["Setting"])


@api_router.patch(
"/settings",
status_code=status.HTTP_200_OK,
description="Upload data from sensors (central, outdoor, external)",
)
async def patch_multiple_settings(
payload: list[SettingPatchWithKey],
session: AsyncSession = Depends(get_session), # noqa: B008
):
"""
Update multiple settings in the database

Args:
payload (List[SettingPatchWithKey]): List of settings to update
session (AsyncSession): The database session
"""
await save_multiple_settings(session, payload)
return
83 changes: 83 additions & 0 deletions app/endpoints/weather.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from starlette import status

from app.db.connection import get_session
from app.schemas import (
CentralData,
ExternalData,
SensorData,
WeatherCurrentResponse,
WeatherPredictionResponse,
WeatherUploadRequest,
)
from app.utils.common import new_data_logic
from app.utils.queries import get_last_data_for_sensors
from app.utils.weather_predict import get_data_weather_prediction

api_router = APIRouter(tags=["Weather"])


@api_router.get(
"/weather/current",
status_code=status.HTTP_200_OK,
response_model=WeatherCurrentResponse,
description="Get current weather",
)
async def get_current_weather(
session: AsyncSession = Depends(get_session), # noqa: B008
):
"""
Get current weather data based on the last data

Args:
session (AsyncSession): The database session
"""
raw_data = await get_last_data_for_sensors(session)

formatted_data = {
"central": CentralData.model_validate(raw_data["central"]),
"outdoor": SensorData.model_validate(raw_data["outdoor"]),
"external_weather": ExternalData.model_validate(raw_data["external_weather"]),
}

return WeatherCurrentResponse(**formatted_data)


@api_router.get(
"/weather/predict",
status_code=status.HTTP_200_OK,
response_model=WeatherPredictionResponse,
description="Get weather prediction",
)
async def get_weather_prediction(
session: AsyncSession = Depends(get_session), # noqa: B008
):
"""
Get weather prediction based on the last data

Args:
session (AsyncSession): The database session
"""
data = await get_data_weather_prediction(session)
return WeatherPredictionResponse(**data)


@api_router.post(
"/weather/upload",
status_code=status.HTTP_200_OK,
description="Upload data from sensors (central, outdoor, external)",
)
async def upload_weather_data(
payload: WeatherUploadRequest,
session: AsyncSession = Depends(get_session), # noqa: B008
):
"""
Upload weather data to the database

Args:
payload (WeatherUploadRequest): The data to be uploaded
session (AsyncSession): The database session
"""
await new_data_logic(session=session, payload=payload)
return
20 changes: 19 additions & 1 deletion app/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
__all__ = []
from app.schemas.setting import SettingPatchWithKey
from app.schemas.weather import (
CentralData,
ExternalData,
SensorData,
WeatherCurrentResponse,
WeatherPredictionResponse,
WeatherUploadRequest,
)

__all__ = [
"SettingPatchWithKey",
"SensorData",
"CentralData",
"ExternalData",
"WeatherCurrentResponse",
"WeatherPredictionResponse",
"WeatherUploadRequest",
]
Loading