Skip to content

Commit

Permalink
Move some configuration out of the .env file and into a config/app.ya…
Browse files Browse the repository at this point in the history
…ml file.
  • Loading branch information
caarmen committed Dec 8, 2024
1 parent ceed624 commit f0d67fe
Show file tree
Hide file tree
Showing 21 changed files with 287 additions and 166 deletions.
24 changes: 1 addition & 23 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,30 +1,8 @@
SERVER_URL=http://localhost:8000/
DATABASE_PATH=/tmp/data/slackhealthbot.db

WITHINGS_CLIENT_ID=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
WITHINGS_CLIENT_SECRET=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
WITHINGS_CALLBACK_URL=http://localhost:8000/

FITBIT_CLIENT_ID=01A2B3
FITBIT_CLIENT_SECRET=0123456789abcdef0123456789abcdef
FITBIT_CLIENT_SUBSCRIBER_VERIFICATION_CODE=0123456789abcdef0123456789abcdef0123456789abcdef1023456789abcdef
FITBIT_POLL_ENABLED=true
FITBIT_POLL_INTERVAL_S=3600

# For FITBIT_ACTIVITY_TYPE_IDS:
# See https://dev.fitbit.com/build/reference/web-api/activity/get-all-activity-types/
# for the list of all supported activity types and their ids.
# Some examples:
# 55001: Spinning
# 90013: Walk
# 90001: Bike
# 90019: Treadmill
# 1071: Outdoor Bike
FITBIT_REALTIME_ACTIVITY_TYPE_IDS=[55001, 90013]
FITBIT_DAILY_ACTIVITY_TYPE_IDS=[90019]
FITBIT_DAILY_ACTIVITY_POST_TIME=23:50
FITBIT_ACTIVITY_RECORD_HISTORY_DAYS=180

SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/abcdefghijklmnopqrstuvwx

SQL_LOG_LEVEL=WARNING
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXXX/abcdefghijklmnopqrstuvwx
53 changes: 53 additions & 0 deletions config/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Configuration of the slack-health-bot application
app:
server_url: "http://localhost:8000/" # The url to access the slack-health-bot server for login.
database_path: "/tmp/data/slackhealthbot.db" # The location to the database file.
logging:
sql_log_level: "WARNING"

# Withings-specific configuration:
# Note that secrets like the client id and client secret are configured in the .env file.
withings:
callback_url: "http://localhost:8000/" # The url that withings will call at the end of SSO.

# Fitbit-specific configuration:
# Note that secrets like the client id and client secret are configured in the .env file.
fitbit:
poll:
enabled: true # If your server can't receive webhook calls from fitbit, activate polling instead, to fetch data from fitbit.
interval_seconds: 3600 # How often to poll fitbit for data.

activities:
history_days: 180 # how far to look back to report new records of best times/durations/calories/etc.
daily_report_time: "23:50" # Time of day (HH:mm)to post daily reports to slack.

activity_types:
# Configuration specific to activity types.
#
# For fitbit activity type ids:
# See https://dev.fitbit.com/build/reference/web-api/activity/get-all-activity-types/
# for the list of all supported activity types and their ids.
# Some examples:
# 55001: Spinning
# 90013: Walk
# 90001: Bike
# 90019: Treadmill
# 1071: Outdoor Bike
#
# supported attributes:
# report_daily: whether a daily summary report should be posted to slack for this activity type
# report_realtime: whether a report should be posted to slack for this activity type as soon as we receive it from fitbit
- name: Treadmill
id: 90019
report_daily: true
report_realtime: false

- name: Spinning
id: 55001
report_daily: false
report_realtime: true

- name: Walk
id: 90013
report_daily: false
report_realtime: true
2 changes: 1 addition & 1 deletion requirements/prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ fastapi==0.115.6
httpx==0.27.2
itsdangerous==2.2.0
Jinja2==3.1.4
pydantic-settings==2.6.1
pydantic-settings[yaml]==2.6.1
pydantic==2.10.3
python-dotenv==1.0.1
python-multipart==0.0.19
Expand Down
6 changes: 3 additions & 3 deletions slackhealthbot/data/database/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

from slackhealthbot.settings import settings

connection_url = f"sqlite+aiosqlite:///{settings.database_path}"
Path(settings.database_path).parent.mkdir(parents=True, exist_ok=True)
connection_url = f"sqlite+aiosqlite:///{settings.app_settings.app.database_path}"
Path(settings.app_settings.app.database_path).parent.mkdir(parents=True, exist_ok=True)
engine = create_async_engine(
connection_url,
connect_args={"check_same_thread": False},
Expand All @@ -16,7 +16,7 @@
autocommit=False, autoflush=False, bind=engine, future=True
)

if settings.sql_log_level.upper() == "DEBUG":
if settings.app_settings.app.logging.sql_log_level.upper() == "DEBUG":

def before_cursor_execute(_conn, _cursor, statement, parameters, *args):
logging.debug(f"{statement}; args={parameters}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ async def do(
await local_fitbit_repo.get_top_daily_activity_stats_by_user_and_activity_type(
fitbit_userid=fitbit_userid,
type_id=daily_activity.type_id,
since=now - dt.timedelta(days=settings.fitbit_activity_record_history_days),
since=now
- dt.timedelta(days=settings.app_settings.fitbit.activities.history_days),
)
)

Expand All @@ -68,5 +69,5 @@ async def do(
slack_alias=user_identity.slack_alias,
activity_name=activity_names.get(daily_activity.type_id, "Unknown"),
history=history,
record_history_days=settings.fitbit_activity_record_history_days,
record_history_days=settings.app_settings.fitbit.activities.history_days,
)
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ async def do(
fitbit_userid=fitbit_userid,
type_id=new_activity_data.type_id,
since=datetime.datetime.now(datetime.timezone.utc)
- datetime.timedelta(days=settings.fitbit_activity_record_history_days),
- datetime.timedelta(
days=settings.app_settings.fitbit.activities.history_days
),
)
)
await usecase_post_activity.do(
Expand All @@ -96,7 +98,7 @@ async def do(
all_time_top_activity_data=all_time_top_activity_stats,
recent_top_activity_data=recent_top_activity_stats,
),
record_history_days=settings.fitbit_activity_record_history_days,
record_history_days=settings.app_settings.fitbit.activities.history_days,
)

return new_activity_data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ async def do(
message = f"""
Oh no <@{slack_alias}>, looks like you were logged out of {service}! 😳.
You'll need to log in again to get your reports:
{settings.server_url}v1/{service}-authorization/{slack_alias}
{settings.app_settings.app.server_url}v1/{service}-authorization/{slack_alias}
"""
await repo.post_message(message)
6 changes: 3 additions & 3 deletions slackhealthbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

@asynccontextmanager
async def lifespan(_app: FastAPI):
logger.configure_logging(settings.sql_log_level)
logger.configure_logging(settings.app_settings.app.logging.sql_log_level)
oauth_withings.configure(
WithingsUpdateTokenUseCase(
request_context_withings_repository,
Expand All @@ -49,7 +49,7 @@ async def lifespan(_app: FastAPI):
)
)
schedule_task = None
if settings.fitbit_poll_enabled:
if settings.app_settings.fitbit.poll.enabled:
schedule_task = await fitbitpoll.schedule_fitbit_poll(
local_fitbit_repo_factory=fitbit_repository_factory(),
remote_fitbit_repo=get_remote_fitbit_repository(),
Expand All @@ -62,7 +62,7 @@ async def lifespan(_app: FastAPI):
local_fitbit_repo_factory=fitbit_repository_factory(),
activity_type_ids=set(settings.fitbit_daily_activity_type_ids),
slack_repo=get_slack_repository(),
post_time=settings.fitbit_daily_activity_post_time,
post_time=settings.app_settings.fitbit.activities.daily_report_time,
)
yield
if schedule_task:
Expand Down
2 changes: 1 addition & 1 deletion slackhealthbot/remoteservices/api/slack/messageapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
async def post_message(message: str):
async with httpx.AsyncClient() as client:
await client.post(
url=str(settings.slack_webhook_url),
url=str(settings.secret_settings.slack_webhook_url),
json={
"text": message,
},
Expand Down
132 changes: 98 additions & 34 deletions slackhealthbot/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@
import datetime as dt
from pathlib import Path

from pydantic import AnyHttpUrl, HttpUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import AnyHttpUrl, BaseModel, HttpUrl
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
SettingsConfigDict,
YamlConfigSettingsSource,
)


@dataclasses.dataclass
Expand All @@ -23,63 +28,122 @@ class FitbitOAuthSettings:
subscriber_verification_code: str


class Settings(BaseSettings):
database_path: Path = "/tmp/data/slackhealthbot.db"
class Poll(BaseModel):
enabled: bool = True
interval_seconds: int = 3600


class ActivityType(BaseModel):
name: str
id: int
report_daily: bool = False
report_realtime: bool = True


class Activities(BaseModel):
daily_report_time: dt.time = dt.time(hour=23, second=50)
history_days: int = 180
activity_types: list[ActivityType]


class Fitbit(BaseModel):
poll: Poll
activities: Activities
base_url: str = "https://api.fitbit.com/"
oauth_scopes: list[str] = ["sleep", "activity"]


class Withings(BaseModel):
callback_url: AnyHttpUrl
base_url: str = "https://wbsapi.withings.net/"
oauth_scopes: list[str] = ["user.metrics", "user.activity"]


class Logging(BaseModel):
sql_log_level: str = "WARNING"


class App(BaseModel):
server_url: AnyHttpUrl
withings_base_url: str = "https://wbsapi.withings.net/"
withings_oauth_scopes: list[str] = ["user.metrics", "user.activity"]
database_path: Path = "/tmp/data/slackhealthbot.db"
logging: Logging


class AppSettings(BaseSettings):
app: App
withings: Withings
fitbit: Fitbit
model_config = SettingsConfigDict(yaml_file="config/app.yaml")

@classmethod
def settings_customise_sources(
cls,
settings_cls: BaseSettings,
*args,
**kwargs,
) -> tuple[PydanticBaseSettingsSource, ...]:
yaml_settings_source = YamlConfigSettingsSource(settings_cls)
return (yaml_settings_source,)


class SecretSettings(BaseSettings):
withings_client_secret: str
withings_client_id: str
withings_callback_url: AnyHttpUrl
fitbit_base_url: str = "https://api.fitbit.com/"
fitbit_oauth_scopes: list[str] = ["sleep", "activity"]
fitbit_client_id: str
fitbit_client_secret: str
fitbit_client_subscriber_verification_code: str
fitbit_poll_interval_s: int = 3600
fitbit_poll_enabled: bool = True
fitbit_realtime_activity_type_ids: list[int] = [
# See https://dev.fitbit.com/build/reference/web-api/activity/get-all-activity-types/
# for the list of all supported activity types and their ids
55001, # Spinning
90013, # Walk
# 90001, # Bike
# 90019, # Treadmill
# 1071, # Outdoor Bike
]
fitbit_daily_activity_type_ids: list[int] = [
90019,
]
fitbit_daily_activity_post_time: dt.time = dt.time(hour=23, second=50)
fitbit_activity_record_history_days: int = 180
slack_webhook_url: HttpUrl
sql_log_level: str = "WARNING"
model_config = SettingsConfigDict(env_file=".env")


@dataclasses.dataclass
class Settings:
app_settings: AppSettings
secret_settings: SecretSettings

@property
def withings_oauth_settings(self):
return WithingsOAuthSettings(
base_url=self.withings_base_url,
oauth_scopes=self.withings_oauth_scopes,
callback_url=self.withings_callback_url,
redirect_uri=f"{self.withings_callback_url}withings-oauth-webhook/",
base_url=self.app_settings.withings.base_url,
oauth_scopes=self.app_settings.withings.oauth_scopes,
callback_url=self.app_settings.withings.callback_url,
redirect_uri=f"{self.app_settings.withings.callback_url}withings-oauth-webhook/",
)

@property
def fitbit_oauth_settings(self):
return FitbitOAuthSettings(
base_url=self.fitbit_base_url,
oauth_scopes=self.fitbit_oauth_scopes,
subscriber_verification_code=self.fitbit_client_subscriber_verification_code,
base_url=self.app_settings.fitbit.base_url,
oauth_scopes=self.app_settings.fitbit.oauth_scopes,
subscriber_verification_code=self.secret_settings.fitbit_client_subscriber_verification_code,
)

@property
def fitbit_realtime_activity_type_ids(self) -> list[int]:
return [
x.id
for x in self.app_settings.fitbit.activities.activity_types
if x.report_realtime
]

@property
def fitbit_daily_activity_type_ids(self) -> list[int]:
return [
x.id
for x in self.app_settings.fitbit.activities.activity_types
if x.report_daily
]

@property
def fitbit_activity_type_ids(self) -> list[int]:
return (
self.fitbit_realtime_activity_type_ids + self.fitbit_daily_activity_type_ids
)


settings = Settings()
settings = Settings(
app_settings=AppSettings(),
secret_settings=SecretSettings(),
)
withings_oauth_settings = settings.withings_oauth_settings
fitbit_oauth_settings = settings.fitbit_oauth_settings
4 changes: 2 additions & 2 deletions slackhealthbot/tasks/fitbitpoll.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ async def schedule_fitbit_poll(
local_fitbit_repo_factory: Callable[[], AsyncContextManager[LocalFitbitRepository]],
remote_fitbit_repo: RemoteFitbitRepository,
slack_repo: RemoteSlackRepository,
initial_delay_s: int = settings.fitbit_poll_interval_s,
initial_delay_s: int = settings.app_settings.fitbit.poll.interval_seconds,
cache: Cache = None,
):
if cache is None:
Expand All @@ -197,6 +197,6 @@ async def run_with_delay():
remote_fitbit_repo=remote_fitbit_repo,
slack_repo=slack_repo,
)
await asyncio.sleep(settings.fitbit_poll_interval_s)
await asyncio.sleep(settings.app_settings.fitbit.poll.interval_seconds)

return asyncio.create_task(run_with_delay())
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ async def test_process_daily_activities(
)

# Mock an empty ok response from the slack webhook
slack_request = respx_mock.post(f"{settings.slack_webhook_url}").mock(
return_value=Response(200)
)
slack_request = respx_mock.post(
f"{settings.secret_settings.slack_webhook_url}"
).mock(return_value=Response(200))

with monkeypatch.context() as mp:
freeze_time(
Expand Down
Loading

0 comments on commit f0d67fe

Please sign in to comment.