Files
duty-teller/site/api-reference/index.html
Nikolay Tatarinov 86f6d66865
All checks were successful
CI / lint-and-test (push) Successful in 17s
chore: add changelog and documentation updates
- Created a new `CHANGELOG.md` file to document all notable changes to the project, adhering to the Keep a Changelog format.
- Updated `CONTRIBUTING.md` to include instructions for building and previewing documentation using MkDocs.
- Added `mkdocs.yml` configuration for documentation generation, including navigation structure and theme settings.
- Enhanced various documentation files, including API reference, architecture overview, configuration reference, and runbook, to provide comprehensive guidance for users and developers.
- Included new sections in the README for changelog and documentation links, improving accessibility to project information.
2026-02-20 15:32:10 +03:00

16650 lines
693 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Telegram bot for team duty shift calendar and group reminder">
<link rel="canonical" href="https://github.com/your-org/duty-teller/api-reference/">
<link rel="prev" href="../runbook/">
<link rel="icon" href="../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.2">
<title>API Reference - Duty Teller</title>
<link rel="stylesheet" href="../assets/stylesheets/main.484c7ddc.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<link rel="stylesheet" href="../assets/_mkdocstrings.css">
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#api-reference" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href=".." title="Duty Teller" class="md-header__button md-logo" aria-label="Duty Teller" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
Duty Teller
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
API Reference
</span>
</div>
</div>
</div>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href=".." title="Duty Teller" class="md-nav__button md-logo" aria-label="Duty Teller" data-md-component="logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
</a>
Duty Teller
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href=".." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../configuration/" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../architecture/" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../import-format/" class="md-nav__link">
<span class="md-ellipsis">
Import format
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../runbook/" class="md-nav__link">
<span class="md-ellipsis">
Runbook
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
API Reference
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
API Reference
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#configuration" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config" class="md-nav__link">
<span class="md-ellipsis">
config
</span>
</a>
<nav class="md-nav" aria-label="config">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.config.Settings" class="md-nav__link">
<span class="md-ellipsis">
Settings
</span>
</a>
<nav class="md-nav" aria-label="Settings">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.config.Settings.from_env" class="md-nav__link">
<span class="md-ellipsis">
from_env
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.can_access_miniapp" class="md-nav__link">
<span class="md-ellipsis">
can_access_miniapp
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.can_access_miniapp_by_phone" class="md-nav__link">
<span class="md-ellipsis">
can_access_miniapp_by_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.is_admin" class="md-nav__link">
<span class="md-ellipsis">
is_admin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.is_admin_by_phone" class="md-nav__link">
<span class="md-ellipsis">
is_admin_by_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.normalize_phone" class="md-nav__link">
<span class="md-ellipsis">
normalize_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.require_bot_token" class="md-nav__link">
<span class="md-ellipsis">
require_bot_token
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#api-fastapi-and-auth" class="md-nav__link">
<span class="md-ellipsis">
API (FastAPI and auth)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api" class="md-nav__link">
<span class="md-ellipsis">
api
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.app" class="md-nav__link">
<span class="md-ellipsis">
app
</span>
</a>
<nav class="md-nav" aria-label="app">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.app.get_personal_calendar_ical" class="md-nav__link">
<span class="md-ellipsis">
get_personal_calendar_ical
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies" class="md-nav__link">
<span class="md-ellipsis">
dependencies
</span>
</a>
<nav class="md-nav" aria-label="dependencies">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.fetch_duties_response" class="md-nav__link">
<span class="md-ellipsis">
fetch_duties_response
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.get_authenticated_username" class="md-nav__link">
<span class="md-ellipsis">
get_authenticated_username
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.get_db_session" class="md-nav__link">
<span class="md-ellipsis">
get_db_session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.get_validated_dates" class="md-nav__link">
<span class="md-ellipsis">
get_validated_dates
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.require_miniapp_username" class="md-nav__link">
<span class="md-ellipsis">
require_miniapp_username
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.telegram_auth" class="md-nav__link">
<span class="md-ellipsis">
telegram_auth
</span>
</a>
<nav class="md-nav" aria-label="telegram_auth">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.telegram_auth.validate_init_data" class="md-nav__link">
<span class="md-ellipsis">
validate_init_data
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.telegram_auth.validate_init_data_with_reason" class="md-nav__link">
<span class="md-ellipsis">
validate_init_data_with_reason
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.calendar_ics" class="md-nav__link">
<span class="md-ellipsis">
calendar_ics
</span>
</a>
<nav class="md-nav" aria-label="calendar_ics">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.calendar_ics.get_calendar_events" class="md-nav__link">
<span class="md-ellipsis">
get_calendar_events
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.personal_calendar_ics" class="md-nav__link">
<span class="md-ellipsis">
personal_calendar_ics
</span>
</a>
<nav class="md-nav" aria-label="personal_calendar_ics">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.personal_calendar_ics.build_personal_ics" class="md-nav__link">
<span class="md-ellipsis">
build_personal_ics
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#database" class="md-nav__link">
<span class="md-ellipsis">
Database
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db" class="md-nav__link">
<span class="md-ellipsis">
db
</span>
</a>
<nav class="md-nav" aria-label="db">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.Base" class="md-nav__link">
<span class="md-ellipsis">
Base
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.Duty" class="md-nav__link">
<span class="md-ellipsis">
Duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.DutyCreate" class="md-nav__link">
<span class="md-ellipsis">
DutyCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.DutyInDb" class="md-nav__link">
<span class="md-ellipsis">
DutyInDb
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.DutyWithUser" class="md-nav__link">
<span class="md-ellipsis">
DutyWithUser
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.User" class="md-nav__link">
<span class="md-ellipsis">
User
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.UserCreate" class="md-nav__link">
<span class="md-ellipsis">
UserCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.UserInDb" class="md-nav__link">
<span class="md-ellipsis">
UserInDb
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.delete_duties_in_range" class="md-nav__link">
<span class="md-ellipsis">
delete_duties_in_range
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_duties" class="md-nav__link">
<span class="md-ellipsis">
get_duties
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_engine" class="md-nav__link">
<span class="md-ellipsis">
get_engine
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_or_create_user" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_or_create_user_by_full_name" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user_by_full_name
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_session" class="md-nav__link">
<span class="md-ellipsis">
get_session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_session_factory" class="md-nav__link">
<span class="md-ellipsis">
get_session_factory
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.init_db" class="md-nav__link">
<span class="md-ellipsis">
init_db
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.insert_duty" class="md-nav__link">
<span class="md-ellipsis">
insert_duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session_scope" class="md-nav__link">
<span class="md-ellipsis">
session_scope
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.set_user_phone" class="md-nav__link">
<span class="md-ellipsis">
set_user_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.update_user_display_name" class="md-nav__link">
<span class="md-ellipsis">
update_user_display_name
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models" class="md-nav__link">
<span class="md-ellipsis">
models
</span>
</a>
<nav class="md-nav" aria-label="models">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.models.Base" class="md-nav__link">
<span class="md-ellipsis">
Base
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.CalendarSubscriptionToken" class="md-nav__link">
<span class="md-ellipsis">
CalendarSubscriptionToken
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.Duty" class="md-nav__link">
<span class="md-ellipsis">
Duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.GroupDutyPin" class="md-nav__link">
<span class="md-ellipsis">
GroupDutyPin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.User" class="md-nav__link">
<span class="md-ellipsis">
User
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas" class="md-nav__link">
<span class="md-ellipsis">
schemas
</span>
</a>
<nav class="md-nav" aria-label="schemas">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.CalendarEvent" class="md-nav__link">
<span class="md-ellipsis">
CalendarEvent
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyBase" class="md-nav__link">
<span class="md-ellipsis">
DutyBase
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyCreate" class="md-nav__link">
<span class="md-ellipsis">
DutyCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyInDb" class="md-nav__link">
<span class="md-ellipsis">
DutyInDb
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyWithUser" class="md-nav__link">
<span class="md-ellipsis">
DutyWithUser
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.UserBase" class="md-nav__link">
<span class="md-ellipsis">
UserBase
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.UserCreate" class="md-nav__link">
<span class="md-ellipsis">
UserCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.UserInDb" class="md-nav__link">
<span class="md-ellipsis">
UserInDb
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session" class="md-nav__link">
<span class="md-ellipsis">
session
</span>
</a>
<nav class="md-nav" aria-label="session">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.session.get_engine" class="md-nav__link">
<span class="md-ellipsis">
get_engine
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session.get_session" class="md-nav__link">
<span class="md-ellipsis">
get_session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session.get_session_factory" class="md-nav__link">
<span class="md-ellipsis">
get_session_factory
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session.session_scope" class="md-nav__link">
<span class="md-ellipsis">
session_scope
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository" class="md-nav__link">
<span class="md-ellipsis">
repository
</span>
</a>
<nav class="md-nav" aria-label="repository">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.repository.create_calendar_token" class="md-nav__link">
<span class="md-ellipsis">
create_calendar_token
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.delete_duties_in_range" class="md-nav__link">
<span class="md-ellipsis">
delete_duties_in_range
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.delete_group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
delete_group_duty_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_all_group_duty_pin_chat_ids" class="md-nav__link">
<span class="md-ellipsis">
get_all_group_duty_pin_chat_ids
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_current_duty" class="md-nav__link">
<span class="md-ellipsis">
get_current_duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_duties" class="md-nav__link">
<span class="md-ellipsis">
get_duties
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_duties_for_user" class="md-nav__link">
<span class="md-ellipsis">
get_duties_for_user
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
get_group_duty_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_next_shift_end" class="md-nav__link">
<span class="md-ellipsis">
get_next_shift_end
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_or_create_user" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_or_create_user_by_full_name" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user_by_full_name
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_user_by_calendar_token" class="md-nav__link">
<span class="md-ellipsis">
get_user_by_calendar_token
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_user_by_telegram_id" class="md-nav__link">
<span class="md-ellipsis">
get_user_by_telegram_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.insert_duty" class="md-nav__link">
<span class="md-ellipsis">
insert_duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.save_group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
save_group_duty_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.set_user_phone" class="md-nav__link">
<span class="md-ellipsis">
set_user_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.update_user_display_name" class="md-nav__link">
<span class="md-ellipsis">
update_user_display_name
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#services" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services" class="md-nav__link">
<span class="md-ellipsis">
services
</span>
</a>
<nav class="md-nav" aria-label="services">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.services.delete_pin" class="md-nav__link">
<span class="md-ellipsis">
delete_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.format_duty_message" class="md-nav__link">
<span class="md-ellipsis">
format_duty_message
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_all_pin_chat_ids" class="md-nav__link">
<span class="md-ellipsis">
get_all_pin_chat_ids
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_duty_message_text" class="md-nav__link">
<span class="md-ellipsis">
get_duty_message_text
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_message_id" class="md-nav__link">
<span class="md-ellipsis">
get_message_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_next_shift_end_utc" class="md-nav__link">
<span class="md-ellipsis">
get_next_shift_end_utc
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.run_import" class="md-nav__link">
<span class="md-ellipsis">
run_import
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.save_pin" class="md-nav__link">
<span class="md-ellipsis">
save_pin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.import_service" class="md-nav__link">
<span class="md-ellipsis">
import_service
</span>
</a>
<nav class="md-nav" aria-label="import_service">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.services.import_service.run_import" class="md-nav__link">
<span class="md-ellipsis">
run_import
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service" class="md-nav__link">
<span class="md-ellipsis">
group_duty_pin_service
</span>
</a>
<nav class="md-nav" aria-label="group_duty_pin_service">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.delete_pin" class="md-nav__link">
<span class="md-ellipsis">
delete_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.format_duty_message" class="md-nav__link">
<span class="md-ellipsis">
format_duty_message
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_all_pin_chat_ids" class="md-nav__link">
<span class="md-ellipsis">
get_all_pin_chat_ids
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_duty_message_text" class="md-nav__link">
<span class="md-ellipsis">
get_duty_message_text
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_message_id" class="md-nav__link">
<span class="md-ellipsis">
get_message_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_next_shift_end_utc" class="md-nav__link">
<span class="md-ellipsis">
get_next_shift_end_utc
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.save_pin" class="md-nav__link">
<span class="md-ellipsis">
save_pin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#handlers" class="md-nav__link">
<span class="md-ellipsis">
Handlers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers" class="md-nav__link">
<span class="md-ellipsis">
handlers
</span>
</a>
<nav class="md-nav" aria-label="handlers">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.register_handlers" class="md-nav__link">
<span class="md-ellipsis">
register_handlers
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands" class="md-nav__link">
<span class="md-ellipsis">
commands
</span>
</a>
<nav class="md-nav" aria-label="commands">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.calendar_link" class="md-nav__link">
<span class="md-ellipsis">
calendar_link
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.help_cmd" class="md-nav__link">
<span class="md-ellipsis">
help_cmd
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.set_phone" class="md-nav__link">
<span class="md-ellipsis">
set_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.start" class="md-nav__link">
<span class="md-ellipsis">
start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule" class="md-nav__link">
<span class="md-ellipsis">
import_duty_schedule
</span>
</a>
<nav class="md-nav" aria-label="import_duty_schedule">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule.handle_duty_schedule_document" class="md-nav__link">
<span class="md-ellipsis">
handle_duty_schedule_document
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule.handle_handover_time_text" class="md-nav__link">
<span class="md-ellipsis">
handle_handover_time_text
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule.import_duty_schedule_cmd" class="md-nav__link">
<span class="md-ellipsis">
import_duty_schedule_cmd
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
group_duty_pin
</span>
</a>
<nav class="md-nav" aria-label="group_duty_pin">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.my_chat_member_handler" class="md-nav__link">
<span class="md-ellipsis">
my_chat_member_handler
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.pin_duty_cmd" class="md-nav__link">
<span class="md-ellipsis">
pin_duty_cmd
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.restore_group_pin_jobs" class="md-nav__link">
<span class="md-ellipsis">
restore_group_pin_jobs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.update_group_pin" class="md-nav__link">
<span class="md-ellipsis">
update_group_pin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.errors" class="md-nav__link">
<span class="md-ellipsis">
errors
</span>
</a>
<nav class="md-nav" aria-label="errors">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.errors.error_handler" class="md-nav__link">
<span class="md-ellipsis">
error_handler
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#importers" class="md-nav__link">
<span class="md-ellipsis">
Importers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers" class="md-nav__link">
<span class="md-ellipsis">
importers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule" class="md-nav__link">
<span class="md-ellipsis">
duty_schedule
</span>
</a>
<nav class="md-nav" aria-label="duty_schedule">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.DutyScheduleEntry" class="md-nav__link">
<span class="md-ellipsis">
DutyScheduleEntry
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.DutyScheduleParseError" class="md-nav__link">
<span class="md-ellipsis">
DutyScheduleParseError
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.DutyScheduleResult" class="md-nav__link">
<span class="md-ellipsis">
DutyScheduleResult
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.parse_duty_schedule" class="md-nav__link">
<span class="md-ellipsis">
parse_duty_schedule
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#configuration" class="md-nav__link">
<span class="md-ellipsis">
Configuration
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config" class="md-nav__link">
<span class="md-ellipsis">
config
</span>
</a>
<nav class="md-nav" aria-label="config">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.config.Settings" class="md-nav__link">
<span class="md-ellipsis">
Settings
</span>
</a>
<nav class="md-nav" aria-label="Settings">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.config.Settings.from_env" class="md-nav__link">
<span class="md-ellipsis">
from_env
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.can_access_miniapp" class="md-nav__link">
<span class="md-ellipsis">
can_access_miniapp
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.can_access_miniapp_by_phone" class="md-nav__link">
<span class="md-ellipsis">
can_access_miniapp_by_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.is_admin" class="md-nav__link">
<span class="md-ellipsis">
is_admin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.is_admin_by_phone" class="md-nav__link">
<span class="md-ellipsis">
is_admin_by_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.normalize_phone" class="md-nav__link">
<span class="md-ellipsis">
normalize_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.config.require_bot_token" class="md-nav__link">
<span class="md-ellipsis">
require_bot_token
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#api-fastapi-and-auth" class="md-nav__link">
<span class="md-ellipsis">
API (FastAPI and auth)
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api" class="md-nav__link">
<span class="md-ellipsis">
api
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.app" class="md-nav__link">
<span class="md-ellipsis">
app
</span>
</a>
<nav class="md-nav" aria-label="app">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.app.get_personal_calendar_ical" class="md-nav__link">
<span class="md-ellipsis">
get_personal_calendar_ical
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies" class="md-nav__link">
<span class="md-ellipsis">
dependencies
</span>
</a>
<nav class="md-nav" aria-label="dependencies">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.fetch_duties_response" class="md-nav__link">
<span class="md-ellipsis">
fetch_duties_response
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.get_authenticated_username" class="md-nav__link">
<span class="md-ellipsis">
get_authenticated_username
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.get_db_session" class="md-nav__link">
<span class="md-ellipsis">
get_db_session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.get_validated_dates" class="md-nav__link">
<span class="md-ellipsis">
get_validated_dates
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.dependencies.require_miniapp_username" class="md-nav__link">
<span class="md-ellipsis">
require_miniapp_username
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.telegram_auth" class="md-nav__link">
<span class="md-ellipsis">
telegram_auth
</span>
</a>
<nav class="md-nav" aria-label="telegram_auth">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.telegram_auth.validate_init_data" class="md-nav__link">
<span class="md-ellipsis">
validate_init_data
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.telegram_auth.validate_init_data_with_reason" class="md-nav__link">
<span class="md-ellipsis">
validate_init_data_with_reason
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.calendar_ics" class="md-nav__link">
<span class="md-ellipsis">
calendar_ics
</span>
</a>
<nav class="md-nav" aria-label="calendar_ics">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.calendar_ics.get_calendar_events" class="md-nav__link">
<span class="md-ellipsis">
get_calendar_events
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.api.personal_calendar_ics" class="md-nav__link">
<span class="md-ellipsis">
personal_calendar_ics
</span>
</a>
<nav class="md-nav" aria-label="personal_calendar_ics">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.api.personal_calendar_ics.build_personal_ics" class="md-nav__link">
<span class="md-ellipsis">
build_personal_ics
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#database" class="md-nav__link">
<span class="md-ellipsis">
Database
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db" class="md-nav__link">
<span class="md-ellipsis">
db
</span>
</a>
<nav class="md-nav" aria-label="db">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.Base" class="md-nav__link">
<span class="md-ellipsis">
Base
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.Duty" class="md-nav__link">
<span class="md-ellipsis">
Duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.DutyCreate" class="md-nav__link">
<span class="md-ellipsis">
DutyCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.DutyInDb" class="md-nav__link">
<span class="md-ellipsis">
DutyInDb
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.DutyWithUser" class="md-nav__link">
<span class="md-ellipsis">
DutyWithUser
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.User" class="md-nav__link">
<span class="md-ellipsis">
User
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.UserCreate" class="md-nav__link">
<span class="md-ellipsis">
UserCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.UserInDb" class="md-nav__link">
<span class="md-ellipsis">
UserInDb
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.delete_duties_in_range" class="md-nav__link">
<span class="md-ellipsis">
delete_duties_in_range
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_duties" class="md-nav__link">
<span class="md-ellipsis">
get_duties
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_engine" class="md-nav__link">
<span class="md-ellipsis">
get_engine
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_or_create_user" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_or_create_user_by_full_name" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user_by_full_name
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_session" class="md-nav__link">
<span class="md-ellipsis">
get_session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.get_session_factory" class="md-nav__link">
<span class="md-ellipsis">
get_session_factory
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.init_db" class="md-nav__link">
<span class="md-ellipsis">
init_db
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.insert_duty" class="md-nav__link">
<span class="md-ellipsis">
insert_duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session_scope" class="md-nav__link">
<span class="md-ellipsis">
session_scope
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.set_user_phone" class="md-nav__link">
<span class="md-ellipsis">
set_user_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.update_user_display_name" class="md-nav__link">
<span class="md-ellipsis">
update_user_display_name
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models" class="md-nav__link">
<span class="md-ellipsis">
models
</span>
</a>
<nav class="md-nav" aria-label="models">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.models.Base" class="md-nav__link">
<span class="md-ellipsis">
Base
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.CalendarSubscriptionToken" class="md-nav__link">
<span class="md-ellipsis">
CalendarSubscriptionToken
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.Duty" class="md-nav__link">
<span class="md-ellipsis">
Duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.GroupDutyPin" class="md-nav__link">
<span class="md-ellipsis">
GroupDutyPin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.models.User" class="md-nav__link">
<span class="md-ellipsis">
User
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas" class="md-nav__link">
<span class="md-ellipsis">
schemas
</span>
</a>
<nav class="md-nav" aria-label="schemas">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.CalendarEvent" class="md-nav__link">
<span class="md-ellipsis">
CalendarEvent
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyBase" class="md-nav__link">
<span class="md-ellipsis">
DutyBase
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyCreate" class="md-nav__link">
<span class="md-ellipsis">
DutyCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyInDb" class="md-nav__link">
<span class="md-ellipsis">
DutyInDb
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.DutyWithUser" class="md-nav__link">
<span class="md-ellipsis">
DutyWithUser
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.UserBase" class="md-nav__link">
<span class="md-ellipsis">
UserBase
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.UserCreate" class="md-nav__link">
<span class="md-ellipsis">
UserCreate
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.schemas.UserInDb" class="md-nav__link">
<span class="md-ellipsis">
UserInDb
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session" class="md-nav__link">
<span class="md-ellipsis">
session
</span>
</a>
<nav class="md-nav" aria-label="session">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.session.get_engine" class="md-nav__link">
<span class="md-ellipsis">
get_engine
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session.get_session" class="md-nav__link">
<span class="md-ellipsis">
get_session
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session.get_session_factory" class="md-nav__link">
<span class="md-ellipsis">
get_session_factory
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.session.session_scope" class="md-nav__link">
<span class="md-ellipsis">
session_scope
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository" class="md-nav__link">
<span class="md-ellipsis">
repository
</span>
</a>
<nav class="md-nav" aria-label="repository">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.db.repository.create_calendar_token" class="md-nav__link">
<span class="md-ellipsis">
create_calendar_token
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.delete_duties_in_range" class="md-nav__link">
<span class="md-ellipsis">
delete_duties_in_range
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.delete_group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
delete_group_duty_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_all_group_duty_pin_chat_ids" class="md-nav__link">
<span class="md-ellipsis">
get_all_group_duty_pin_chat_ids
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_current_duty" class="md-nav__link">
<span class="md-ellipsis">
get_current_duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_duties" class="md-nav__link">
<span class="md-ellipsis">
get_duties
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_duties_for_user" class="md-nav__link">
<span class="md-ellipsis">
get_duties_for_user
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
get_group_duty_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_next_shift_end" class="md-nav__link">
<span class="md-ellipsis">
get_next_shift_end
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_or_create_user" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_or_create_user_by_full_name" class="md-nav__link">
<span class="md-ellipsis">
get_or_create_user_by_full_name
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_user_by_calendar_token" class="md-nav__link">
<span class="md-ellipsis">
get_user_by_calendar_token
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.get_user_by_telegram_id" class="md-nav__link">
<span class="md-ellipsis">
get_user_by_telegram_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.insert_duty" class="md-nav__link">
<span class="md-ellipsis">
insert_duty
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.save_group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
save_group_duty_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.set_user_phone" class="md-nav__link">
<span class="md-ellipsis">
set_user_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.db.repository.update_user_display_name" class="md-nav__link">
<span class="md-ellipsis">
update_user_display_name
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#services" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services" class="md-nav__link">
<span class="md-ellipsis">
services
</span>
</a>
<nav class="md-nav" aria-label="services">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.services.delete_pin" class="md-nav__link">
<span class="md-ellipsis">
delete_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.format_duty_message" class="md-nav__link">
<span class="md-ellipsis">
format_duty_message
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_all_pin_chat_ids" class="md-nav__link">
<span class="md-ellipsis">
get_all_pin_chat_ids
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_duty_message_text" class="md-nav__link">
<span class="md-ellipsis">
get_duty_message_text
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_message_id" class="md-nav__link">
<span class="md-ellipsis">
get_message_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.get_next_shift_end_utc" class="md-nav__link">
<span class="md-ellipsis">
get_next_shift_end_utc
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.run_import" class="md-nav__link">
<span class="md-ellipsis">
run_import
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.save_pin" class="md-nav__link">
<span class="md-ellipsis">
save_pin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.import_service" class="md-nav__link">
<span class="md-ellipsis">
import_service
</span>
</a>
<nav class="md-nav" aria-label="import_service">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.services.import_service.run_import" class="md-nav__link">
<span class="md-ellipsis">
run_import
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service" class="md-nav__link">
<span class="md-ellipsis">
group_duty_pin_service
</span>
</a>
<nav class="md-nav" aria-label="group_duty_pin_service">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.delete_pin" class="md-nav__link">
<span class="md-ellipsis">
delete_pin
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.format_duty_message" class="md-nav__link">
<span class="md-ellipsis">
format_duty_message
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_all_pin_chat_ids" class="md-nav__link">
<span class="md-ellipsis">
get_all_pin_chat_ids
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_duty_message_text" class="md-nav__link">
<span class="md-ellipsis">
get_duty_message_text
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_message_id" class="md-nav__link">
<span class="md-ellipsis">
get_message_id
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.get_next_shift_end_utc" class="md-nav__link">
<span class="md-ellipsis">
get_next_shift_end_utc
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.services.group_duty_pin_service.save_pin" class="md-nav__link">
<span class="md-ellipsis">
save_pin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#handlers" class="md-nav__link">
<span class="md-ellipsis">
Handlers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers" class="md-nav__link">
<span class="md-ellipsis">
handlers
</span>
</a>
<nav class="md-nav" aria-label="handlers">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.register_handlers" class="md-nav__link">
<span class="md-ellipsis">
register_handlers
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands" class="md-nav__link">
<span class="md-ellipsis">
commands
</span>
</a>
<nav class="md-nav" aria-label="commands">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.calendar_link" class="md-nav__link">
<span class="md-ellipsis">
calendar_link
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.help_cmd" class="md-nav__link">
<span class="md-ellipsis">
help_cmd
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.set_phone" class="md-nav__link">
<span class="md-ellipsis">
set_phone
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.commands.start" class="md-nav__link">
<span class="md-ellipsis">
start
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule" class="md-nav__link">
<span class="md-ellipsis">
import_duty_schedule
</span>
</a>
<nav class="md-nav" aria-label="import_duty_schedule">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule.handle_duty_schedule_document" class="md-nav__link">
<span class="md-ellipsis">
handle_duty_schedule_document
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule.handle_handover_time_text" class="md-nav__link">
<span class="md-ellipsis">
handle_handover_time_text
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.import_duty_schedule.import_duty_schedule_cmd" class="md-nav__link">
<span class="md-ellipsis">
import_duty_schedule_cmd
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin" class="md-nav__link">
<span class="md-ellipsis">
group_duty_pin
</span>
</a>
<nav class="md-nav" aria-label="group_duty_pin">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.my_chat_member_handler" class="md-nav__link">
<span class="md-ellipsis">
my_chat_member_handler
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.pin_duty_cmd" class="md-nav__link">
<span class="md-ellipsis">
pin_duty_cmd
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.restore_group_pin_jobs" class="md-nav__link">
<span class="md-ellipsis">
restore_group_pin_jobs
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.group_duty_pin.update_group_pin" class="md-nav__link">
<span class="md-ellipsis">
update_group_pin
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#duty_teller.handlers.errors" class="md-nav__link">
<span class="md-ellipsis">
errors
</span>
</a>
<nav class="md-nav" aria-label="errors">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.handlers.errors.error_handler" class="md-nav__link">
<span class="md-ellipsis">
error_handler
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#importers" class="md-nav__link">
<span class="md-ellipsis">
Importers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers" class="md-nav__link">
<span class="md-ellipsis">
importers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule" class="md-nav__link">
<span class="md-ellipsis">
duty_schedule
</span>
</a>
<nav class="md-nav" aria-label="duty_schedule">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.DutyScheduleEntry" class="md-nav__link">
<span class="md-ellipsis">
DutyScheduleEntry
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.DutyScheduleParseError" class="md-nav__link">
<span class="md-ellipsis">
DutyScheduleParseError
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.DutyScheduleResult" class="md-nav__link">
<span class="md-ellipsis">
DutyScheduleResult
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#duty_teller.importers.duty_schedule.parse_duty_schedule" class="md-nav__link">
<span class="md-ellipsis">
parse_duty_schedule
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<h1 id="api-reference">API Reference</h1>
<p>Generated from the <code>duty_teller</code> package. The following subpackages and modules are included.</p>
<h2 id="configuration">Configuration</h2>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.config" class="doc doc-heading">
<code>duty_teller.config</code>
</h2>
<div class="doc doc-contents first">
<p>Load configuration from environment (e.g. .env via python-dotenv).</p>
<p>BOT_TOKEN is not validated on import; call require_bot_token() in the entry point
when running the bot.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-class">
<h3 id="duty_teller.config.Settings" class="doc doc-heading">
<code>Settings</code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-dataclass"><code>dataclass</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Injectable settings built from environment. Used in tests or when env is overridden.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 55</span>
<span class="normal"> 56</span>
<span class="normal"> 57</span>
<span class="normal"> 58</span>
<span class="normal"> 59</span>
<span class="normal"> 60</span>
<span class="normal"> 61</span>
<span class="normal"> 62</span>
<span class="normal"> 63</span>
<span class="normal"> 64</span>
<span class="normal"> 65</span>
<span class="normal"> 66</span>
<span class="normal"> 67</span>
<span class="normal"> 68</span>
<span class="normal"> 69</span>
<span class="normal"> 70</span>
<span class="normal"> 71</span>
<span class="normal"> 72</span>
<span class="normal"> 73</span>
<span class="normal"> 74</span>
<span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@dataclass</span><span class="p">(</span><span class="n">frozen</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="k">class</span><span class="w"> </span><span class="nc">Settings</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Injectable settings built from environment. Used in tests or when env is overridden.&quot;&quot;&quot;</span>
<span class="n">bot_token</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">mini_app_base_url</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">http_port</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">allowed_usernames</span><span class="p">:</span> <span class="nb">set</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">admin_usernames</span><span class="p">:</span> <span class="nb">set</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">allowed_phones</span><span class="p">:</span> <span class="nb">set</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">admin_phones</span><span class="p">:</span> <span class="nb">set</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">mini_app_skip_auth</span><span class="p">:</span> <span class="nb">bool</span>
<span class="n">init_data_max_age_seconds</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">cors_origins</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>
<span class="n">external_calendar_ics_url</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">duty_display_tz</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">default_language</span><span class="p">:</span> <span class="nb">str</span>
<span class="nd">@classmethod</span>
<span class="k">def</span><span class="w"> </span><span class="nf">from_env</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="s2">&quot;Settings&quot;</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Build Settings from current environment (same logic as module-level variables).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Settings instance with all fields populated from env.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">bot_token</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;BOT_TOKEN&quot;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span>
<span class="n">raw_allowed</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ALLOWED_USERNAMES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">allowed</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">&quot;@&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">raw_allowed</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;,&quot;</span><span class="p">)</span> <span class="k">if</span> <span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">raw_admin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ADMIN_USERNAMES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">admin</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">&quot;@&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">raw_admin</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;,&quot;</span><span class="p">)</span> <span class="k">if</span> <span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">allowed_phones</span> <span class="o">=</span> <span class="n">_parse_phone_list</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ALLOWED_PHONES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">))</span>
<span class="n">admin_phones</span> <span class="o">=</span> <span class="n">_parse_phone_list</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ADMIN_PHONES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">))</span>
<span class="n">raw_cors</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;CORS_ORIGINS&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">cors</span> <span class="o">=</span> <span class="p">(</span>
<span class="p">[</span><span class="n">_o</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">_o</span> <span class="ow">in</span> <span class="n">raw_cors</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;,&quot;</span><span class="p">)</span> <span class="k">if</span> <span class="n">_o</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span>
<span class="k">if</span> <span class="n">raw_cors</span> <span class="ow">and</span> <span class="n">raw_cors</span> <span class="o">!=</span> <span class="s2">&quot;*&quot;</span>
<span class="k">else</span> <span class="p">[</span><span class="s2">&quot;*&quot;</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">cls</span><span class="p">(</span>
<span class="n">bot_token</span><span class="o">=</span><span class="n">bot_token</span><span class="p">,</span>
<span class="n">database_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;DATABASE_URL&quot;</span><span class="p">,</span> <span class="s2">&quot;sqlite:///data/duty_teller.db&quot;</span><span class="p">),</span>
<span class="n">mini_app_base_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;MINI_APP_BASE_URL&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s2">&quot;/&quot;</span><span class="p">),</span>
<span class="n">http_port</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;HTTP_PORT&quot;</span><span class="p">,</span> <span class="s2">&quot;8080&quot;</span><span class="p">)),</span>
<span class="n">allowed_usernames</span><span class="o">=</span><span class="n">allowed</span><span class="p">,</span>
<span class="n">admin_usernames</span><span class="o">=</span><span class="n">admin</span><span class="p">,</span>
<span class="n">allowed_phones</span><span class="o">=</span><span class="n">allowed_phones</span><span class="p">,</span>
<span class="n">admin_phones</span><span class="o">=</span><span class="n">admin_phones</span><span class="p">,</span>
<span class="n">mini_app_skip_auth</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;MINI_APP_SKIP_AUTH&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="ow">in</span> <span class="p">(</span><span class="s2">&quot;1&quot;</span><span class="p">,</span> <span class="s2">&quot;true&quot;</span><span class="p">,</span> <span class="s2">&quot;yes&quot;</span><span class="p">),</span>
<span class="n">init_data_max_age_seconds</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;INIT_DATA_MAX_AGE_SECONDS&quot;</span><span class="p">,</span> <span class="s2">&quot;0&quot;</span><span class="p">)),</span>
<span class="n">cors_origins</span><span class="o">=</span><span class="n">cors</span><span class="p">,</span>
<span class="n">external_calendar_ics_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span>
<span class="s2">&quot;EXTERNAL_CALENDAR_ICS_URL&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span>
<span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">(),</span>
<span class="n">duty_display_tz</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;DUTY_DISPLAY_TZ&quot;</span><span class="p">,</span> <span class="s2">&quot;Europe/Moscow&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="ow">or</span> <span class="s2">&quot;Europe/Moscow&quot;</span><span class="p">,</span>
<span class="n">default_language</span><span class="o">=</span><span class="n">_normalize_default_language</span><span class="p">(</span>
<span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;DEFAULT_LANGUAGE&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="p">),</span>
<span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h4 id="duty_teller.config.Settings.from_env" class="doc doc-heading">
<code class="highlight language-python"><span class="n">from_env</span><span class="p">()</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-classmethod"><code>classmethod</code></small>
</span>
</h4>
<div class="doc doc-contents ">
<p>Build Settings from current environment (same logic as module-level variables).</p>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="Settings
dataclass
(duty_teller.config.Settings)" href="#duty_teller.config.Settings">Settings</a></code>
</td>
<td>
<div class="doc-md-description">
<p>Settings instance with all fields populated from env.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 74</span>
<span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@classmethod</span>
<span class="k">def</span><span class="w"> </span><span class="nf">from_env</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="s2">&quot;Settings&quot;</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Build Settings from current environment (same logic as module-level variables).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Settings instance with all fields populated from env.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">bot_token</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;BOT_TOKEN&quot;</span><span class="p">)</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span>
<span class="n">raw_allowed</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ALLOWED_USERNAMES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">allowed</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">&quot;@&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">raw_allowed</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;,&quot;</span><span class="p">)</span> <span class="k">if</span> <span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">raw_admin</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ADMIN_USERNAMES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">admin</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">&quot;@&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">raw_admin</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;,&quot;</span><span class="p">)</span> <span class="k">if</span> <span class="n">s</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="p">}</span>
<span class="n">allowed_phones</span> <span class="o">=</span> <span class="n">_parse_phone_list</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ALLOWED_PHONES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">))</span>
<span class="n">admin_phones</span> <span class="o">=</span> <span class="n">_parse_phone_list</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;ADMIN_PHONES&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">))</span>
<span class="n">raw_cors</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;CORS_ORIGINS&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">cors</span> <span class="o">=</span> <span class="p">(</span>
<span class="p">[</span><span class="n">_o</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">_o</span> <span class="ow">in</span> <span class="n">raw_cors</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;,&quot;</span><span class="p">)</span> <span class="k">if</span> <span class="n">_o</span><span class="o">.</span><span class="n">strip</span><span class="p">()]</span>
<span class="k">if</span> <span class="n">raw_cors</span> <span class="ow">and</span> <span class="n">raw_cors</span> <span class="o">!=</span> <span class="s2">&quot;*&quot;</span>
<span class="k">else</span> <span class="p">[</span><span class="s2">&quot;*&quot;</span><span class="p">]</span>
<span class="p">)</span>
<span class="k">return</span> <span class="bp">cls</span><span class="p">(</span>
<span class="n">bot_token</span><span class="o">=</span><span class="n">bot_token</span><span class="p">,</span>
<span class="n">database_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;DATABASE_URL&quot;</span><span class="p">,</span> <span class="s2">&quot;sqlite:///data/duty_teller.db&quot;</span><span class="p">),</span>
<span class="n">mini_app_base_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;MINI_APP_BASE_URL&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s2">&quot;/&quot;</span><span class="p">),</span>
<span class="n">http_port</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;HTTP_PORT&quot;</span><span class="p">,</span> <span class="s2">&quot;8080&quot;</span><span class="p">)),</span>
<span class="n">allowed_usernames</span><span class="o">=</span><span class="n">allowed</span><span class="p">,</span>
<span class="n">admin_usernames</span><span class="o">=</span><span class="n">admin</span><span class="p">,</span>
<span class="n">allowed_phones</span><span class="o">=</span><span class="n">allowed_phones</span><span class="p">,</span>
<span class="n">admin_phones</span><span class="o">=</span><span class="n">admin_phones</span><span class="p">,</span>
<span class="n">mini_app_skip_auth</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;MINI_APP_SKIP_AUTH&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="ow">in</span> <span class="p">(</span><span class="s2">&quot;1&quot;</span><span class="p">,</span> <span class="s2">&quot;true&quot;</span><span class="p">,</span> <span class="s2">&quot;yes&quot;</span><span class="p">),</span>
<span class="n">init_data_max_age_seconds</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;INIT_DATA_MAX_AGE_SECONDS&quot;</span><span class="p">,</span> <span class="s2">&quot;0&quot;</span><span class="p">)),</span>
<span class="n">cors_origins</span><span class="o">=</span><span class="n">cors</span><span class="p">,</span>
<span class="n">external_calendar_ics_url</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span>
<span class="s2">&quot;EXTERNAL_CALENDAR_ICS_URL&quot;</span><span class="p">,</span> <span class="s2">&quot;&quot;</span>
<span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">(),</span>
<span class="n">duty_display_tz</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;DUTY_DISPLAY_TZ&quot;</span><span class="p">,</span> <span class="s2">&quot;Europe/Moscow&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="ow">or</span> <span class="s2">&quot;Europe/Moscow&quot;</span><span class="p">,</span>
<span class="n">default_language</span><span class="o">=</span><span class="n">_normalize_default_language</span><span class="p">(</span>
<span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&quot;DEFAULT_LANGUAGE&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="p">),</span>
<span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.config.can_access_miniapp" class="doc doc-heading">
<code class="highlight language-python"><span class="n">can_access_miniapp</span><span class="p">(</span><span class="n">username</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Check if username is allowed to open the calendar Miniapp.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>username</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram username (with or without @; case-insensitive).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="bool">bool</span></code>
</td>
<td>
<div class="doc-md-description">
<p>True if in ALLOWED_USERNAMES or ADMIN_USERNAMES.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">172</span>
<span class="normal">173</span>
<span class="normal">174</span>
<span class="normal">175</span>
<span class="normal">176</span>
<span class="normal">177</span>
<span class="normal">178</span>
<span class="normal">179</span>
<span class="normal">180</span>
<span class="normal">181</span>
<span class="normal">182</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">can_access_miniapp</span><span class="p">(</span><span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Check if username is allowed to open the calendar Miniapp.</span>
<span class="sd"> Args:</span>
<span class="sd"> username: Telegram username (with or without @; case-insensitive).</span>
<span class="sd"> Returns:</span>
<span class="sd"> True if in ALLOWED_USERNAMES or ADMIN_USERNAMES.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">u</span> <span class="o">=</span> <span class="p">(</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="k">return</span> <span class="n">u</span> <span class="ow">in</span> <span class="n">ALLOWED_USERNAMES</span> <span class="ow">or</span> <span class="n">u</span> <span class="ow">in</span> <span class="n">ADMIN_USERNAMES</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.config.can_access_miniapp_by_phone" class="doc doc-heading">
<code class="highlight language-python"><span class="n">can_access_miniapp_by_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Check if phone (set via /set_phone) is allowed to open the Miniapp.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>phone</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Raw phone string or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="bool">bool</span></code>
</td>
<td>
<div class="doc-md-description">
<p>True if normalized phone is in ALLOWED_PHONES or ADMIN_PHONES.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">185</span>
<span class="normal">186</span>
<span class="normal">187</span>
<span class="normal">188</span>
<span class="normal">189</span>
<span class="normal">190</span>
<span class="normal">191</span>
<span class="normal">192</span>
<span class="normal">193</span>
<span class="normal">194</span>
<span class="normal">195</span>
<span class="normal">196</span>
<span class="normal">197</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">can_access_miniapp_by_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Check if phone (set via /set_phone) is allowed to open the Miniapp.</span>
<span class="sd"> Args:</span>
<span class="sd"> phone: Raw phone string or None.</span>
<span class="sd"> Returns:</span>
<span class="sd"> True if normalized phone is in ALLOWED_PHONES or ADMIN_PHONES.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">normalized</span> <span class="o">=</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">normalized</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">False</span>
<span class="k">return</span> <span class="n">normalized</span> <span class="ow">in</span> <span class="n">ALLOWED_PHONES</span> <span class="ow">or</span> <span class="n">normalized</span> <span class="ow">in</span> <span class="n">ADMIN_PHONES</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.config.is_admin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">is_admin</span><span class="p">(</span><span class="n">username</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Check if Telegram username is in ADMIN_USERNAMES.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>username</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram username (with or without @; case-insensitive).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="bool">bool</span></code>
</td>
<td>
<div class="doc-md-description">
<p>True if in ADMIN_USERNAMES.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">160</span>
<span class="normal">161</span>
<span class="normal">162</span>
<span class="normal">163</span>
<span class="normal">164</span>
<span class="normal">165</span>
<span class="normal">166</span>
<span class="normal">167</span>
<span class="normal">168</span>
<span class="normal">169</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">is_admin</span><span class="p">(</span><span class="n">username</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Check if Telegram username is in ADMIN_USERNAMES.</span>
<span class="sd"> Args:</span>
<span class="sd"> username: Telegram username (with or without @; case-insensitive).</span>
<span class="sd"> Returns:</span>
<span class="sd"> True if in ADMIN_USERNAMES.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="p">(</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">ADMIN_USERNAMES</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.config.is_admin_by_phone" class="doc doc-heading">
<code class="highlight language-python"><span class="n">is_admin_by_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Check if phone is in ADMIN_PHONES.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>phone</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Raw phone string or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="bool">bool</span></code>
</td>
<td>
<div class="doc-md-description">
<p>True if normalized phone is in ADMIN_PHONES.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">200</span>
<span class="normal">201</span>
<span class="normal">202</span>
<span class="normal">203</span>
<span class="normal">204</span>
<span class="normal">205</span>
<span class="normal">206</span>
<span class="normal">207</span>
<span class="normal">208</span>
<span class="normal">209</span>
<span class="normal">210</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">is_admin_by_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Check if phone is in ADMIN_PHONES.</span>
<span class="sd"> Args:</span>
<span class="sd"> phone: Raw phone string or None.</span>
<span class="sd"> Returns:</span>
<span class="sd"> True if normalized phone is in ADMIN_PHONES.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">normalized</span> <span class="o">=</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">)</span>
<span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="n">normalized</span> <span class="ow">and</span> <span class="n">normalized</span> <span class="ow">in</span> <span class="n">ADMIN_PHONES</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.config.normalize_phone" class="doc doc-heading">
<code class="highlight language-python"><span class="n">normalize_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return phone as digits only (spaces, +, parentheses, dashes removed).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>phone</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Raw phone string or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Digits-only string, or empty string if None or empty.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">normalize_phone</span><span class="p">(</span><span class="n">phone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return phone as digits only (spaces, +, parentheses, dashes removed).</span>
<span class="sd"> Args:</span>
<span class="sd"> phone: Raw phone string or None.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Digits-only string, or empty string if None or empty.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">phone</span> <span class="ow">or</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">phone</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">&quot;&quot;</span>
<span class="k">return</span> <span class="n">_PHONE_DIGITS_RE</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="s2">&quot;&quot;</span><span class="p">,</span> <span class="n">phone</span><span class="o">.</span><span class="n">strip</span><span class="p">())</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.config.require_bot_token" class="doc doc-heading">
<code class="highlight language-python"><span class="n">require_bot_token</span><span class="p">()</span></code>
</h3>
<div class="doc doc-contents ">
<p>Raise SystemExit with a clear message if BOT_TOKEN is not set.</p>
<p>Call from the application entry point (e.g. main.py or duty_teller.run) so the
process exits with a helpful message instead of failing later.</p>
<p><span class="doc-section-title">Raises:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="SystemExit">SystemExit</span></code>
</td>
<td>
<div class="doc-md-description">
<p>If BOT_TOKEN is empty.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/config.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">213</span>
<span class="normal">214</span>
<span class="normal">215</span>
<span class="normal">216</span>
<span class="normal">217</span>
<span class="normal">218</span>
<span class="normal">219</span>
<span class="normal">220</span>
<span class="normal">221</span>
<span class="normal">222</span>
<span class="normal">223</span>
<span class="normal">224</span>
<span class="normal">225</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">require_bot_token</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Raise SystemExit with a clear message if BOT_TOKEN is not set.</span>
<span class="sd"> Call from the application entry point (e.g. main.py or duty_teller.run) so the</span>
<span class="sd"> process exits with a helpful message instead of failing later.</span>
<span class="sd"> Raises:</span>
<span class="sd"> SystemExit: If BOT_TOKEN is empty.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">BOT_TOKEN</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">SystemExit</span><span class="p">(</span>
<span class="s2">&quot;BOT_TOKEN is not set. Copy .env.example to .env and set your token from @BotFather.&quot;</span>
<span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div><h2 id="api-fastapi-and-auth">API (FastAPI and auth)</h2>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.api" class="doc doc-heading">
<code>duty_teller.api</code>
</h2>
<div class="doc doc-contents first">
<p>HTTP API for the calendar Mini App: duties, calendar events, and static webapp.</p>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.api.app" class="doc doc-heading">
<code>duty_teller.api.app</code>
</h2>
<div class="doc doc-contents first">
<p>FastAPI app: /api/duties, /api/calendar-events, personal ICS, and static webapp at /app.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.app.get_personal_calendar_ical" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_personal_calendar_ical</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="n">Depends</span><span class="p">(</span><span class="n">get_db_session</span><span class="p">))</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return ICS calendar with only the subscribing user's duties.
No Telegram auth; access is by secret token in the URL.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/app.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 74</span>
<span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@app</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<span class="s2">&quot;/api/calendar/ical/</span><span class="si">{token}</span><span class="s2">.ics&quot;</span><span class="p">,</span>
<span class="n">summary</span><span class="o">=</span><span class="s2">&quot;Personal calendar ICS&quot;</span><span class="p">,</span>
<span class="n">description</span><span class="o">=</span><span class="s2">&quot;Returns an ICS calendar with only the subscribing user&#39;s duties. No Telegram auth; access is by secret token in the URL.&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">def</span><span class="w"> </span><span class="nf">get_personal_calendar_ical</span><span class="p">(</span>
<span class="n">token</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span> <span class="o">=</span> <span class="n">Depends</span><span class="p">(</span><span class="n">get_db_session</span><span class="p">),</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Response</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;</span>
<span class="sd"> Return ICS calendar with only the subscribing user&#39;s duties.</span>
<span class="sd"> No Telegram auth; access is by secret token in the URL.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_user_by_calendar_token</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="n">Response</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">404</span><span class="p">,</span> <span class="n">content</span><span class="o">=</span><span class="s2">&quot;Not found&quot;</span><span class="p">)</span>
<span class="n">today</span> <span class="o">=</span> <span class="n">date</span><span class="o">.</span><span class="n">today</span><span class="p">()</span>
<span class="n">from_date</span> <span class="o">=</span> <span class="p">(</span><span class="n">today</span> <span class="o">-</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">365</span><span class="p">))</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">to_date</span> <span class="o">=</span> <span class="p">(</span><span class="n">today</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">365</span> <span class="o">*</span> <span class="mi">2</span><span class="p">))</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">duties_with_name</span> <span class="o">=</span> <span class="n">get_duties_for_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">from_date</span><span class="o">=</span><span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="o">=</span><span class="n">to_date</span>
<span class="p">)</span>
<span class="n">ics_bytes</span> <span class="o">=</span> <span class="n">build_personal_ics</span><span class="p">(</span><span class="n">duties_with_name</span><span class="p">)</span>
<span class="k">return</span> <span class="n">Response</span><span class="p">(</span>
<span class="n">content</span><span class="o">=</span><span class="n">ics_bytes</span><span class="p">,</span>
<span class="n">media_type</span><span class="o">=</span><span class="s2">&quot;text/calendar; charset=utf-8&quot;</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.api.dependencies" class="doc doc-heading">
<code>duty_teller.api.dependencies</code>
</h2>
<div class="doc doc-contents first">
<p>FastAPI dependencies: DB session, Miniapp auth (initData/allowlist), date validation.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.dependencies.fetch_duties_response" class="doc doc-heading">
<code class="highlight language-python"><span class="n">fetch_duties_response</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Load duties in range and return as DutyWithUser list for API response.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<a class="autorefs autorefs-internal" title="DutyWithUser (duty_teller.db.schemas.DutyWithUser)" href="#duty_teller.db.schemas.DutyWithUser">DutyWithUser</a>]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of DutyWithUser (id, user_id, start_at, end_at, full_name, event_type).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/dependencies.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">180</span>
<span class="normal">181</span>
<span class="normal">182</span>
<span class="normal">183</span>
<span class="normal">184</span>
<span class="normal">185</span>
<span class="normal">186</span>
<span class="normal">187</span>
<span class="normal">188</span>
<span class="normal">189</span>
<span class="normal">190</span>
<span class="normal">191</span>
<span class="normal">192</span>
<span class="normal">193</span>
<span class="normal">194</span>
<span class="normal">195</span>
<span class="normal">196</span>
<span class="normal">197</span>
<span class="normal">198</span>
<span class="normal">199</span>
<span class="normal">200</span>
<span class="normal">201</span>
<span class="normal">202</span>
<span class="normal">203</span>
<span class="normal">204</span>
<span class="normal">205</span>
<span class="normal">206</span>
<span class="normal">207</span>
<span class="normal">208</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">fetch_duties_response</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="n">DutyWithUser</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Load duties in range and return as DutyWithUser list for API response.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of DutyWithUser (id, user_id, start_at, end_at, full_name, event_type).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">rows</span> <span class="o">=</span> <span class="n">get_duties</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">from_date</span><span class="o">=</span><span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="o">=</span><span class="n">to_date</span><span class="p">)</span>
<span class="k">return</span> <span class="p">[</span>
<span class="n">DutyWithUser</span><span class="p">(</span>
<span class="nb">id</span><span class="o">=</span><span class="n">duty</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">user_id</span><span class="o">=</span><span class="n">duty</span><span class="o">.</span><span class="n">user_id</span><span class="p">,</span>
<span class="n">start_at</span><span class="o">=</span><span class="n">duty</span><span class="o">.</span><span class="n">start_at</span><span class="p">,</span>
<span class="n">end_at</span><span class="o">=</span><span class="n">duty</span><span class="o">.</span><span class="n">end_at</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">event_type</span><span class="o">=</span><span class="p">(</span>
<span class="n">duty</span><span class="o">.</span><span class="n">event_type</span>
<span class="k">if</span> <span class="n">duty</span><span class="o">.</span><span class="n">event_type</span> <span class="ow">in</span> <span class="p">(</span><span class="s2">&quot;duty&quot;</span><span class="p">,</span> <span class="s2">&quot;unavailable&quot;</span><span class="p">,</span> <span class="s2">&quot;vacation&quot;</span><span class="p">)</span>
<span class="k">else</span> <span class="s2">&quot;duty&quot;</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="k">for</span> <span class="n">duty</span><span class="p">,</span> <span class="n">full_name</span> <span class="ow">in</span> <span class="n">rows</span>
<span class="p">]</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.dependencies.get_authenticated_username" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_authenticated_username</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">x_telegram_init_data</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return identifier for miniapp auth (username or full_name or id:...); empty if skip-auth.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>request</code>
</td>
<td>
<code><span title="fastapi.Request">Request</span></code>
</td>
<td>
<div class="doc-md-description">
<p>FastAPI request (client host for private-IP bypass).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>x_telegram_init_data</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Raw X-Telegram-Init-Data header value.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session (for phone allowlist lookup).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Username, full_name, or "id:<telegram_id>"; empty string if MINI_APP_SKIP_AUTH</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>or private IP and no initData.</p>
</div>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Raises:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="fastapi.HTTPException">HTTPException</span></code>
</td>
<td>
<div class="doc-md-description">
<p>403 if initData missing/invalid or user not in allowlist.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/dependencies.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span>
<span class="normal">129</span>
<span class="normal">130</span>
<span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span>
<span class="normal">136</span>
<span class="normal">137</span>
<span class="normal">138</span>
<span class="normal">139</span>
<span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span>
<span class="normal">143</span>
<span class="normal">144</span>
<span class="normal">145</span>
<span class="normal">146</span>
<span class="normal">147</span>
<span class="normal">148</span>
<span class="normal">149</span>
<span class="normal">150</span>
<span class="normal">151</span>
<span class="normal">152</span>
<span class="normal">153</span>
<span class="normal">154</span>
<span class="normal">155</span>
<span class="normal">156</span>
<span class="normal">157</span>
<span class="normal">158</span>
<span class="normal">159</span>
<span class="normal">160</span>
<span class="normal">161</span>
<span class="normal">162</span>
<span class="normal">163</span>
<span class="normal">164</span>
<span class="normal">165</span>
<span class="normal">166</span>
<span class="normal">167</span>
<span class="normal">168</span>
<span class="normal">169</span>
<span class="normal">170</span>
<span class="normal">171</span>
<span class="normal">172</span>
<span class="normal">173</span>
<span class="normal">174</span>
<span class="normal">175</span>
<span class="normal">176</span>
<span class="normal">177</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_authenticated_username</span><span class="p">(</span>
<span class="n">request</span><span class="p">:</span> <span class="n">Request</span><span class="p">,</span>
<span class="n">x_telegram_init_data</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return identifier for miniapp auth (username or full_name or id:...); empty if skip-auth.</span>
<span class="sd"> Args:</span>
<span class="sd"> request: FastAPI request (client host for private-IP bypass).</span>
<span class="sd"> x_telegram_init_data: Raw X-Telegram-Init-Data header value.</span>
<span class="sd"> session: DB session (for phone allowlist lookup).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Username, full_name, or &quot;id:&lt;telegram_id&gt;&quot;; empty string if MINI_APP_SKIP_AUTH</span>
<span class="sd"> or private IP and no initData.</span>
<span class="sd"> Raises:</span>
<span class="sd"> HTTPException: 403 if initData missing/invalid or user not in allowlist.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">config</span><span class="o">.</span><span class="n">MINI_APP_SKIP_AUTH</span><span class="p">:</span>
<span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;allowing without any auth check (MINI_APP_SKIP_AUTH is set)&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="s2">&quot;&quot;</span>
<span class="n">init_data</span> <span class="o">=</span> <span class="p">(</span><span class="n">x_telegram_init_data</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">init_data</span><span class="p">:</span>
<span class="n">client_host</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">client</span><span class="o">.</span><span class="n">host</span> <span class="k">if</span> <span class="n">request</span><span class="o">.</span><span class="n">client</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">_is_private_client</span><span class="p">(</span><span class="n">client_host</span><span class="p">):</span>
<span class="k">return</span> <span class="s2">&quot;&quot;</span>
<span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;no X-Telegram-Init-Data header (client=</span><span class="si">%s</span><span class="s2">)&quot;</span><span class="p">,</span> <span class="n">client_host</span><span class="p">)</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">_lang_from_accept_language</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;Accept-Language&quot;</span><span class="p">))</span>
<span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">403</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;api.open_from_telegram&quot;</span><span class="p">))</span>
<span class="n">max_age</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">INIT_DATA_MAX_AGE_SECONDS</span> <span class="ow">or</span> <span class="kc">None</span>
<span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">auth_reason</span><span class="p">,</span> <span class="n">lang</span> <span class="o">=</span> <span class="n">validate_init_data_with_reason</span><span class="p">(</span>
<span class="n">init_data</span><span class="p">,</span> <span class="n">config</span><span class="o">.</span><span class="n">BOT_TOKEN</span><span class="p">,</span> <span class="n">max_age_seconds</span><span class="o">=</span><span class="n">max_age</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">auth_reason</span> <span class="o">!=</span> <span class="s2">&quot;ok&quot;</span><span class="p">:</span>
<span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;initData validation failed: </span><span class="si">%s</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">auth_reason</span><span class="p">)</span>
<span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span>
<span class="n">status_code</span><span class="o">=</span><span class="mi">403</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="n">_auth_error_detail</span><span class="p">(</span><span class="n">auth_reason</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">username</span> <span class="ow">and</span> <span class="n">config</span><span class="o">.</span><span class="n">can_access_miniapp</span><span class="p">(</span><span class="n">username</span><span class="p">):</span>
<span class="k">return</span> <span class="n">username</span>
<span class="n">failed_phone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">telegram_user_id</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_user_by_telegram_id</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span> <span class="ow">and</span> <span class="n">user</span><span class="o">.</span><span class="n">phone</span> <span class="ow">and</span> <span class="n">config</span><span class="o">.</span><span class="n">can_access_miniapp_by_phone</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="p">):</span>
<span class="k">return</span> <span class="n">username</span> <span class="ow">or</span> <span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">full_name</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span> <span class="ow">or</span> <span class="sa">f</span><span class="s2">&quot;id:</span><span class="si">{</span><span class="n">telegram_user_id</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">if</span> <span class="n">user</span> <span class="ow">and</span> <span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="p">:</span>
<span class="n">failed_phone</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">normalize_phone</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="p">)</span>
<span class="n">log</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
<span class="s2">&quot;username/phone not in allowlist (username=</span><span class="si">%s</span><span class="s2">, telegram_id=</span><span class="si">%s</span><span class="s2">, phone=</span><span class="si">%s</span><span class="s2">)&quot;</span><span class="p">,</span>
<span class="n">username</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="p">,</span>
<span class="n">failed_phone</span> <span class="k">if</span> <span class="n">failed_phone</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">403</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;api.access_denied&quot;</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.dependencies.get_db_session" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_db_session</span><span class="p">()</span></code>
</h3>
<div class="doc doc-contents ">
<p>Yield a DB session for the request; closed automatically by FastAPI.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/dependencies.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">86</span>
<span class="normal">87</span>
<span class="normal">88</span>
<span class="normal">89</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_db_session</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="n">Generator</span><span class="p">[</span><span class="n">Session</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Yield a DB session for the request; closed automatically by FastAPI.&quot;&quot;&quot;</span>
<span class="k">with</span> <span class="n">session_scope</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">DATABASE_URL</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
<span class="k">yield</span> <span class="n">session</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.dependencies.get_validated_dates" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_validated_dates</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">from_date</span><span class="o">=</span><span class="n">Query</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;ISO date YYYY-MM-DD&#39;</span><span class="p">,</span> <span class="n">alias</span><span class="o">=</span><span class="s1">&#39;from&#39;</span><span class="p">),</span> <span class="n">to_date</span><span class="o">=</span><span class="n">Query</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s1">&#39;ISO date YYYY-MM-DD&#39;</span><span class="p">,</span> <span class="n">alias</span><span class="o">=</span><span class="s1">&#39;to&#39;</span><span class="p">))</span></code>
</h3>
<div class="doc doc-contents ">
<p>Validate from/to date query params; use Accept-Language for error messages.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>request</code>
</td>
<td>
<code><span title="fastapi.Request">Request</span></code>
</td>
<td>
<div class="doc-md-description">
<p>FastAPI request (for Accept-Language).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<code><span title="fastapi.Query">Query</span>(..., description=&#39;ISO date YYYY-MM-DD&#39;, alias=&#39;from&#39;)</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<code><span title="fastapi.Query">Query</span>(..., description=&#39;ISO date YYYY-MM-DD&#39;, alias=&#39;to&#39;)</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="tuple">tuple</span>[<span title="str">str</span>, <span title="str">str</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>(from_date, to_date) as strings.</p>
</div>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Raises:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="fastapi.HTTPException">HTTPException</span></code>
</td>
<td>
<div class="doc-md-description">
<p>400 if format invalid or from_date &gt; to_date.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/dependencies.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">63</span>
<span class="normal">64</span>
<span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span>
<span class="normal">73</span>
<span class="normal">74</span>
<span class="normal">75</span>
<span class="normal">76</span>
<span class="normal">77</span>
<span class="normal">78</span>
<span class="normal">79</span>
<span class="normal">80</span>
<span class="normal">81</span>
<span class="normal">82</span>
<span class="normal">83</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_validated_dates</span><span class="p">(</span>
<span class="n">request</span><span class="p">:</span> <span class="n">Request</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Query</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&quot;ISO date YYYY-MM-DD&quot;</span><span class="p">,</span> <span class="n">alias</span><span class="o">=</span><span class="s2">&quot;from&quot;</span><span class="p">),</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">Query</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">description</span><span class="o">=</span><span class="s2">&quot;ISO date YYYY-MM-DD&quot;</span><span class="p">,</span> <span class="n">alias</span><span class="o">=</span><span class="s2">&quot;to&quot;</span><span class="p">),</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Validate from/to date query params; use Accept-Language for error messages.</span>
<span class="sd"> Args:</span>
<span class="sd"> request: FastAPI request (for Accept-Language).</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> (from_date, to_date) as strings.</span>
<span class="sd"> Raises:</span>
<span class="sd"> HTTPException: 400 if format invalid or from_date &gt; to_date.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">_lang_from_accept_language</span><span class="p">(</span><span class="n">request</span><span class="o">.</span><span class="n">headers</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;Accept-Language&quot;</span><span class="p">))</span>
<span class="n">_validate_duty_dates</span><span class="p">(</span><span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
<span class="k">return</span> <span class="p">(</span><span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.dependencies.require_miniapp_username" class="doc doc-heading">
<code class="highlight language-python"><span class="n">require_miniapp_username</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">x_telegram_init_data</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">session</span><span class="o">=</span><span class="n">Depends</span><span class="p">(</span><span class="n">get_db_session</span><span class="p">))</span></code>
</h3>
<div class="doc doc-contents ">
<p>FastAPI dependency: require valid Miniapp auth; return username/identifier.</p>
<p><span class="doc-section-title">Raises:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="fastapi.HTTPException">HTTPException</span></code>
</td>
<td>
<div class="doc-md-description">
<p>403 if initData missing/invalid or user not in allowlist.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/dependencies.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">require_miniapp_username</span><span class="p">(</span>
<span class="n">request</span><span class="p">:</span> <span class="n">Request</span><span class="p">,</span>
<span class="n">x_telegram_init_data</span><span class="p">:</span> <span class="n">Annotated</span><span class="p">[</span>
<span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">,</span> <span class="n">Header</span><span class="p">(</span><span class="n">alias</span><span class="o">=</span><span class="s2">&quot;X-Telegram-Init-Data&quot;</span><span class="p">)</span>
<span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span> <span class="o">=</span> <span class="n">Depends</span><span class="p">(</span><span class="n">get_db_session</span><span class="p">),</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;FastAPI dependency: require valid Miniapp auth; return username/identifier.</span>
<span class="sd"> Raises:</span>
<span class="sd"> HTTPException: 403 if initData missing/invalid or user not in allowlist.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_authenticated_username</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="n">x_telegram_init_data</span><span class="p">,</span> <span class="n">session</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.api.telegram_auth" class="doc doc-heading">
<code>duty_teller.api.telegram_auth</code>
</h2>
<div class="doc doc-contents first">
<p>Validate Telegram Web App initData and extract user username.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.telegram_auth.validate_init_data" class="doc doc-heading">
<code class="highlight language-python"><span class="n">validate_init_data</span><span class="p">(</span><span class="n">init_data</span><span class="p">,</span> <span class="n">bot_token</span><span class="p">,</span> <span class="n">max_age_seconds</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Validate Telegram Web App initData and return username if valid.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>init_data</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Raw initData string from tgWebAppData.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>bot_token</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Bot token (must match the bot that signed the data).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>max_age_seconds</code>
</td>
<td>
<code><span title="int">int</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Reject if auth_date older than this; None to disable.</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Username (lowercase, no @) or None if validation fails.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/telegram_auth.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">13</span>
<span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">validate_init_data</span><span class="p">(</span>
<span class="n">init_data</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">bot_token</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">max_age_seconds</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Validate Telegram Web App initData and return username if valid.</span>
<span class="sd"> Args:</span>
<span class="sd"> init_data: Raw initData string from tgWebAppData.</span>
<span class="sd"> bot_token: Bot token (must match the bot that signed the data).</span>
<span class="sd"> max_age_seconds: Reject if auth_date older than this; None to disable.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Username (lowercase, no @) or None if validation fails.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">_</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">validate_init_data_with_reason</span><span class="p">(</span>
<span class="n">init_data</span><span class="p">,</span> <span class="n">bot_token</span><span class="p">,</span> <span class="n">max_age_seconds</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">username</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.telegram_auth.validate_init_data_with_reason" class="doc doc-heading">
<code class="highlight language-python"><span class="n">validate_init_data_with_reason</span><span class="p">(</span><span class="n">init_data</span><span class="p">,</span> <span class="n">bot_token</span><span class="p">,</span> <span class="n">max_age_seconds</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Validate initData signature and return user id, username, reason, and lang.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>init_data</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Raw initData string from tgWebAppData.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>bot_token</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Bot token (must match the bot that signed the data).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>max_age_seconds</code>
</td>
<td>
<code><span title="int">int</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Reject if auth_date older than this; None to disable.</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="int">int</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Tuple (telegram_user_id, username, reason, lang). reason is one of: "ok",</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>"empty", "no_hash", "hash_mismatch", "auth_date_expired", "no_user",</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>"user_invalid", "no_user_id". lang is from user.language_code normalized</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>to 'ru' or 'en'; 'en' when no user. On success: (user.id, username or None,</p>
</div>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code><span title="tuple">tuple</span>[<span title="int">int</span> | None, <span title="str">str</span> | None, <span title="str">str</span>, <span title="str">str</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>"ok", lang).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/telegram_auth.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 42</span>
<span class="normal"> 43</span>
<span class="normal"> 44</span>
<span class="normal"> 45</span>
<span class="normal"> 46</span>
<span class="normal"> 47</span>
<span class="normal"> 48</span>
<span class="normal"> 49</span>
<span class="normal"> 50</span>
<span class="normal"> 51</span>
<span class="normal"> 52</span>
<span class="normal"> 53</span>
<span class="normal"> 54</span>
<span class="normal"> 55</span>
<span class="normal"> 56</span>
<span class="normal"> 57</span>
<span class="normal"> 58</span>
<span class="normal"> 59</span>
<span class="normal"> 60</span>
<span class="normal"> 61</span>
<span class="normal"> 62</span>
<span class="normal"> 63</span>
<span class="normal"> 64</span>
<span class="normal"> 65</span>
<span class="normal"> 66</span>
<span class="normal"> 67</span>
<span class="normal"> 68</span>
<span class="normal"> 69</span>
<span class="normal"> 70</span>
<span class="normal"> 71</span>
<span class="normal"> 72</span>
<span class="normal"> 73</span>
<span class="normal"> 74</span>
<span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">validate_init_data_with_reason</span><span class="p">(</span>
<span class="n">init_data</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">bot_token</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">max_age_seconds</span><span class="p">:</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">,</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">,</span> <span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Validate initData signature and return user id, username, reason, and lang.</span>
<span class="sd"> Args:</span>
<span class="sd"> init_data: Raw initData string from tgWebAppData.</span>
<span class="sd"> bot_token: Bot token (must match the bot that signed the data).</span>
<span class="sd"> max_age_seconds: Reject if auth_date older than this; None to disable.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Tuple (telegram_user_id, username, reason, lang). reason is one of: &quot;ok&quot;,</span>
<span class="sd"> &quot;empty&quot;, &quot;no_hash&quot;, &quot;hash_mismatch&quot;, &quot;auth_date_expired&quot;, &quot;no_user&quot;,</span>
<span class="sd"> &quot;user_invalid&quot;, &quot;no_user_id&quot;. lang is from user.language_code normalized</span>
<span class="sd"> to &#39;ru&#39; or &#39;en&#39;; &#39;en&#39; when no user. On success: (user.id, username or None,</span>
<span class="sd"> &quot;ok&quot;, lang).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">init_data</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">bot_token</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;empty&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="n">init_data</span> <span class="o">=</span> <span class="n">init_data</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">params</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">part</span> <span class="ow">in</span> <span class="n">init_data</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;&amp;&quot;</span><span class="p">):</span>
<span class="k">if</span> <span class="s2">&quot;=&quot;</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">part</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">key</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">value</span> <span class="o">=</span> <span class="n">part</span><span class="o">.</span><span class="n">partition</span><span class="p">(</span><span class="s2">&quot;=&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">key</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">params</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="o">=</span> <span class="n">value</span>
<span class="n">hash_val</span> <span class="o">=</span> <span class="n">params</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">&quot;hash&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">hash_val</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;no_hash&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="n">data_pairs</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">params</span><span class="o">.</span><span class="n">items</span><span class="p">())</span>
<span class="c1"># Data-check string: key=value with URL-decoded values (per Telegram example)</span>
<span class="n">data_string</span> <span class="o">=</span> <span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">k</span><span class="si">}</span><span class="s2">=</span><span class="si">{</span><span class="n">unquote</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">data_pairs</span><span class="p">)</span>
<span class="c1"># HMAC-SHA256(key=WebAppData, message=bot_token) per reference implementations</span>
<span class="n">secret_key</span> <span class="o">=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
<span class="sa">b</span><span class="s2">&quot;WebAppData&quot;</span><span class="p">,</span>
<span class="n">msg</span><span class="o">=</span><span class="n">bot_token</span><span class="o">.</span><span class="n">encode</span><span class="p">(),</span>
<span class="n">digestmod</span><span class="o">=</span><span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">,</span>
<span class="p">)</span><span class="o">.</span><span class="n">digest</span><span class="p">()</span>
<span class="n">computed</span> <span class="o">=</span> <span class="n">hmac</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
<span class="n">secret_key</span><span class="p">,</span>
<span class="n">msg</span><span class="o">=</span><span class="n">data_string</span><span class="o">.</span><span class="n">encode</span><span class="p">(),</span>
<span class="n">digestmod</span><span class="o">=</span><span class="n">hashlib</span><span class="o">.</span><span class="n">sha256</span><span class="p">,</span>
<span class="p">)</span><span class="o">.</span><span class="n">hexdigest</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">computed</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="n">hash_val</span><span class="o">.</span><span class="n">lower</span><span class="p">()):</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;hash_mismatch&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">max_age_seconds</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">max_age_seconds</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
<span class="n">auth_date_raw</span> <span class="o">=</span> <span class="n">params</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;auth_date&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">auth_date_raw</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;auth_date_expired&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">auth_date</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">auth_date_raw</span><span class="p">))</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">ValueError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;auth_date_expired&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">auth_date</span> <span class="o">&gt;</span> <span class="n">max_age_seconds</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;auth_date_expired&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="n">user_raw</span> <span class="o">=</span> <span class="n">params</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;user&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user_raw</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;no_user&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">user_raw</span><span class="p">))</span>
<span class="k">except</span> <span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">JSONDecodeError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;user_invalid&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">user</span><span class="p">,</span> <span class="nb">dict</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;user_invalid&quot;</span><span class="p">,</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">_normalize_lang</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;language_code&quot;</span><span class="p">))</span>
<span class="n">raw_id</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;id&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">raw_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;no_user_id&quot;</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">telegram_user_id</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">raw_id</span><span class="p">)</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">TypeError</span><span class="p">,</span> <span class="ne">ValueError</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;no_user_id&quot;</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;username&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">username</span> <span class="ow">and</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">username</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">username</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span><span class="o">.</span><span class="n">lstrip</span><span class="p">(</span><span class="s2">&quot;@&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">username</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="p">(</span><span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="s2">&quot;ok&quot;</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.api.calendar_ics" class="doc doc-heading">
<code>duty_teller.api.calendar_ics</code>
</h2>
<div class="doc doc-contents first">
<p>Fetch and parse external ICS calendar; in-memory cache with 7-day TTL.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.calendar_ics.get_calendar_events" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_calendar_events</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Fetch ICS from URL and return events in the given date range.</p>
<p>Uses in-memory cache with TTL 7 days. Recurring events are skipped.
On fetch or parse error returns an empty list.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>url</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>URL of the ICS calendar.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="dict">dict</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of dicts with keys "date" (YYYY-MM-DD) and "summary". Empty on error.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/calendar_ics.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span>
<span class="normal">129</span>
<span class="normal">130</span>
<span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_calendar_events</span><span class="p">(</span>
<span class="n">url</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">dict</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Fetch ICS from URL and return events in the given date range.</span>
<span class="sd"> Uses in-memory cache with TTL 7 days. Recurring events are skipped.</span>
<span class="sd"> On fetch or parse error returns an empty list.</span>
<span class="sd"> Args:</span>
<span class="sd"> url: URL of the ICS calendar.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of dicts with keys &quot;date&quot; (YYYY-MM-DD) and &quot;summary&quot;. Empty on error.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">url</span> <span class="ow">or</span> <span class="n">from_date</span> <span class="o">&gt;</span> <span class="n">to_date</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span><span class="o">.</span><span class="n">timestamp</span><span class="p">()</span>
<span class="n">raw</span><span class="p">:</span> <span class="nb">bytes</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">if</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">_ics_cache</span><span class="p">:</span>
<span class="n">cached_at</span><span class="p">,</span> <span class="n">cached_raw</span> <span class="o">=</span> <span class="n">_ics_cache</span><span class="p">[</span><span class="n">url</span><span class="p">]</span>
<span class="k">if</span> <span class="n">now</span> <span class="o">-</span> <span class="n">cached_at</span> <span class="o">&lt;</span> <span class="n">CACHE_TTL_SECONDS</span><span class="p">:</span>
<span class="n">raw</span> <span class="o">=</span> <span class="n">cached_raw</span>
<span class="k">if</span> <span class="n">raw</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">raw</span> <span class="o">=</span> <span class="n">_fetch_ics</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">if</span> <span class="n">raw</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="n">_ics_cache</span><span class="p">[</span><span class="n">url</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">now</span><span class="p">,</span> <span class="n">raw</span><span class="p">)</span>
<span class="k">return</span> <span class="n">_get_events_from_ics</span><span class="p">(</span><span class="n">raw</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.api.personal_calendar_ics" class="doc doc-heading">
<code>duty_teller.api.personal_calendar_ics</code>
</h2>
<div class="doc doc-contents first">
<p>Generate ICS calendar containing only one user's duties (for subscription link).</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.api.personal_calendar_ics.build_personal_ics" class="doc doc-heading">
<code class="highlight language-python"><span class="n">build_personal_ics</span><span class="p">(</span><span class="n">duties_with_name</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Build a VCALENDAR (ICS) with one VEVENT per duty.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>duties_with_name</code>
</td>
<td>
<code><span title="list">list</span>[<span title="tuple">tuple</span>[<a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a>, <span title="str">str</span>]]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of (Duty, full_name). full_name is available for
DESCRIPTION; SUMMARY is taken from event_type (duty/unavailable/vacation).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="bytes">bytes</span></code>
</td>
<td>
<div class="doc-md-description">
<p>ICS file content as bytes (UTF-8).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/api/personal_calendar_ics.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">build_personal_ics</span><span class="p">(</span><span class="n">duties_with_name</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="n">Duty</span><span class="p">,</span> <span class="nb">str</span><span class="p">]])</span> <span class="o">-&gt;</span> <span class="nb">bytes</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Build a VCALENDAR (ICS) with one VEVENT per duty.</span>
<span class="sd"> Args:</span>
<span class="sd"> duties_with_name: List of (Duty, full_name). full_name is available for</span>
<span class="sd"> DESCRIPTION; SUMMARY is taken from event_type (duty/unavailable/vacation).</span>
<span class="sd"> Returns:</span>
<span class="sd"> ICS file content as bytes (UTF-8).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">cal</span> <span class="o">=</span> <span class="n">Calendar</span><span class="p">()</span>
<span class="n">cal</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;prodid&quot;</span><span class="p">,</span> <span class="s2">&quot;-//Duty Teller//Personal Calendar//EN&quot;</span><span class="p">)</span>
<span class="n">cal</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;version&quot;</span><span class="p">,</span> <span class="s2">&quot;2.0&quot;</span><span class="p">)</span>
<span class="n">cal</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;calscale&quot;</span><span class="p">,</span> <span class="s2">&quot;GREGORIAN&quot;</span><span class="p">)</span>
<span class="k">for</span> <span class="n">duty</span><span class="p">,</span> <span class="n">_full_name</span> <span class="ow">in</span> <span class="n">duties_with_name</span><span class="p">:</span>
<span class="n">event</span> <span class="o">=</span> <span class="n">Event</span><span class="p">()</span>
<span class="n">start_dt</span> <span class="o">=</span> <span class="n">_parse_utc_iso</span><span class="p">(</span><span class="n">duty</span><span class="o">.</span><span class="n">start_at</span><span class="p">)</span>
<span class="n">end_dt</span> <span class="o">=</span> <span class="n">_parse_utc_iso</span><span class="p">(</span><span class="n">duty</span><span class="o">.</span><span class="n">end_at</span><span class="p">)</span>
<span class="c1"># Ensure timezone-aware for icalendar</span>
<span class="k">if</span> <span class="n">start_dt</span><span class="o">.</span><span class="n">tzinfo</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">start_dt</span> <span class="o">=</span> <span class="n">start_dt</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">tzinfo</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>
<span class="k">if</span> <span class="n">end_dt</span><span class="o">.</span><span class="n">tzinfo</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">end_dt</span> <span class="o">=</span> <span class="n">end_dt</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">tzinfo</span><span class="o">=</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>
<span class="n">event</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;dtstart&quot;</span><span class="p">,</span> <span class="n">start_dt</span><span class="p">)</span>
<span class="n">event</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;dtend&quot;</span><span class="p">,</span> <span class="n">end_dt</span><span class="p">)</span>
<span class="n">summary</span> <span class="o">=</span> <span class="n">SUMMARY_BY_TYPE</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
<span class="n">duty</span><span class="o">.</span><span class="n">event_type</span> <span class="k">if</span> <span class="n">duty</span><span class="o">.</span><span class="n">event_type</span> <span class="k">else</span> <span class="s2">&quot;duty&quot;</span><span class="p">,</span> <span class="s2">&quot;Duty&quot;</span>
<span class="p">)</span>
<span class="n">event</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;summary&quot;</span><span class="p">,</span> <span class="n">summary</span><span class="p">)</span>
<span class="n">event</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;uid&quot;</span><span class="p">,</span> <span class="sa">f</span><span class="s2">&quot;duty-</span><span class="si">{</span><span class="n">duty</span><span class="o">.</span><span class="n">id</span><span class="si">}</span><span class="s2">@duty-teller&quot;</span><span class="p">)</span>
<span class="n">event</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="s2">&quot;dtstamp&quot;</span><span class="p">,</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">))</span>
<span class="n">cal</span><span class="o">.</span><span class="n">add_component</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
<span class="k">return</span> <span class="n">cal</span><span class="o">.</span><span class="n">to_ical</span><span class="p">()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div><h2 id="database">Database</h2>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.db" class="doc doc-heading">
<code>duty_teller.db</code>
</h2>
<div class="doc doc-contents first">
<p>Database layer: SQLAlchemy models, Pydantic schemas, repository, init.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.Base" class="doc doc-heading">
<code>Base</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="sqlalchemy.orm.DeclarativeBase">DeclarativeBase</span></code></p>
<p>Declarative base for all models.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 7</span>
<span class="normal"> 8</span>
<span class="normal"> 9</span>
<span class="normal">10</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Base</span><span class="p">(</span><span class="n">DeclarativeBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Declarative base for all models.&quot;&quot;&quot;</span>
<span class="k">pass</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.Duty" class="doc doc-heading">
<code>Duty</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Base (duty_teller.db.models.Base)" href="#duty_teller.db.models.Base">Base</a></code></p>
<p>Single duty/unavailable/vacation slot (UTC start_at/end_at, event_type).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Duty</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Single duty/unavailable/vacation slot (UTC start_at/end_at, event_type).&quot;&quot;&quot;</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">&quot;duties&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">user_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">Integer</span><span class="p">,</span> <span class="n">ForeignKey</span><span class="p">(</span><span class="s2">&quot;users.id&quot;</span><span class="p">),</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
<span class="c1"># UTC, ISO 8601 with Z suffix (e.g. 2025-01-15T09:00:00Z)</span>
<span class="n">start_at</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">end_at</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="c1"># duty | unavailable | vacation</span>
<span class="n">event_type</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">server_default</span><span class="o">=</span><span class="s2">&quot;duty&quot;</span><span class="p">)</span>
<span class="n">user</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="s2">&quot;User&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="s2">&quot;User&quot;</span><span class="p">,</span> <span class="n">back_populates</span><span class="o">=</span><span class="s2">&quot;duties&quot;</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.DutyCreate" class="doc doc-heading">
<code>DutyCreate</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="DutyBase (duty_teller.db.schemas.DutyBase)" href="#duty_teller.db.schemas.DutyBase">DutyBase</a></code></p>
<p>Duty creation payload.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyCreate</span><span class="p">(</span><span class="n">DutyBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty creation payload.&quot;&quot;&quot;</span>
<span class="k">pass</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.DutyInDb" class="doc doc-heading">
<code>DutyInDb</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="DutyBase (duty_teller.db.schemas.DutyBase)" href="#duty_teller.db.schemas.DutyBase">DutyBase</a></code></p>
<p>Duty as stored in DB (includes id).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyInDb</span><span class="p">(</span><span class="n">DutyBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty as stored in DB (includes id).&quot;&quot;&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">from_attributes</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.DutyWithUser" class="doc doc-heading">
<code>DutyWithUser</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="DutyInDb (duty_teller.db.schemas.DutyInDb)" href="#duty_teller.db.schemas.DutyInDb">DutyInDb</a></code></p>
<p>Duty with full_name and event_type for calendar display.</p>
<p>event_type: only these values are returned; unknown DB values are mapped to "duty" in the API.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyWithUser</span><span class="p">(</span><span class="n">DutyInDb</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty with full_name and event_type for calendar display.</span>
<span class="sd"> event_type: only these values are returned; unknown DB values are mapped to &quot;duty&quot; in the API.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">event_type</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">&quot;duty&quot;</span><span class="p">,</span> <span class="s2">&quot;unavailable&quot;</span><span class="p">,</span> <span class="s2">&quot;vacation&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;duty&quot;</span>
<span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">from_attributes</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.User" class="doc doc-heading">
<code>User</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Base (duty_teller.db.models.Base)" href="#duty_teller.db.models.Base">Base</a></code></p>
<p>Telegram user and display name; may have telegram_user_id=None for import-only users.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">13</span>
<span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">User</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Telegram user and display name; may have telegram_user_id=None for import-only users.&quot;&quot;&quot;</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">&quot;users&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">BigInteger</span><span class="p">,</span> <span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span>
<span class="p">)</span>
<span class="n">full_name</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">username</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">first_name</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">last_name</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">phone</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">name_manually_edited</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">Boolean</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">server_default</span><span class="o">=</span><span class="s2">&quot;0&quot;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
<span class="n">duties</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="s2">&quot;Duty&quot;</span><span class="p">]]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="s2">&quot;Duty&quot;</span><span class="p">,</span> <span class="n">back_populates</span><span class="o">=</span><span class="s2">&quot;user&quot;</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.UserCreate" class="doc doc-heading">
<code>UserCreate</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="UserBase (duty_teller.db.schemas.UserBase)" href="#duty_teller.db.schemas.UserBase">UserBase</a></code></p>
<p>User creation payload including Telegram user id.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">UserCreate</span><span class="p">(</span><span class="n">UserBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;User creation payload including Telegram user id.&quot;&quot;&quot;</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.UserInDb" class="doc doc-heading">
<code>UserInDb</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="UserBase (duty_teller.db.schemas.UserBase)" href="#duty_teller.db.schemas.UserBase">UserBase</a></code></p>
<p>User as stored in DB (includes id and telegram_user_id).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">UserInDb</span><span class="p">(</span><span class="n">UserBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;User as stored in DB (includes id and telegram_user_id).&quot;&quot;&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">from_attributes</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.delete_duties_in_range" class="doc doc-heading">
<code class="highlight language-python"><span class="n">delete_duties_in_range</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Delete all duties of the user that overlap the given date range.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>User id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Number of duties deleted.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">138</span>
<span class="normal">139</span>
<span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span>
<span class="normal">143</span>
<span class="normal">144</span>
<span class="normal">145</span>
<span class="normal">146</span>
<span class="normal">147</span>
<span class="normal">148</span>
<span class="normal">149</span>
<span class="normal">150</span>
<span class="normal">151</span>
<span class="normal">152</span>
<span class="normal">153</span>
<span class="normal">154</span>
<span class="normal">155</span>
<span class="normal">156</span>
<span class="normal">157</span>
<span class="normal">158</span>
<span class="normal">159</span>
<span class="normal">160</span>
<span class="normal">161</span>
<span class="normal">162</span>
<span class="normal">163</span>
<span class="normal">164</span>
<span class="normal">165</span>
<span class="normal">166</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">delete_duties_in_range</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Delete all duties of the user that overlap the given date range.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> user_id: User id.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Number of duties deleted.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">to_next</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">to_date</span> <span class="o">+</span> <span class="s2">&quot;T00:00:00&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">q</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">user_id</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;</span> <span class="n">to_next</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;=</span> <span class="n">from_date</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">count</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="n">count</span><span class="p">()</span>
<span class="n">q</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">synchronize_session</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">return</span> <span class="n">count</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.get_duties" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_duties</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return duties overlapping the given date range with user full_name.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="tuple">tuple</span>[<a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a>, <span title="str">str</span>]]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of (Duty, full_name) tuples.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">169</span>
<span class="normal">170</span>
<span class="normal">171</span>
<span class="normal">172</span>
<span class="normal">173</span>
<span class="normal">174</span>
<span class="normal">175</span>
<span class="normal">176</span>
<span class="normal">177</span>
<span class="normal">178</span>
<span class="normal">179</span>
<span class="normal">180</span>
<span class="normal">181</span>
<span class="normal">182</span>
<span class="normal">183</span>
<span class="normal">184</span>
<span class="normal">185</span>
<span class="normal">186</span>
<span class="normal">187</span>
<span class="normal">188</span>
<span class="normal">189</span>
<span class="normal">190</span>
<span class="normal">191</span>
<span class="normal">192</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_duties</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="n">Duty</span><span class="p">,</span> <span class="nb">str</span><span class="p">]]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return duties overlapping the given date range with user full_name.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of (Duty, full_name) tuples.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">to_date_next</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">to_date</span> <span class="o">+</span> <span class="s2">&quot;T00:00:00&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">q</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">,</span> <span class="n">User</span><span class="o">.</span><span class="n">full_name</span><span class="p">)</span>
<span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">User</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;</span> <span class="n">to_date_next</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;=</span> <span class="n">from_date</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">all</span><span class="p">())</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.get_engine" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return cached SQLAlchemy engine for the given URL (one per process).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return cached SQLAlchemy engine for the given URL (one per process).&quot;&quot;&quot;</span>
<span class="k">global</span> <span class="n">_engine</span>
<span class="k">if</span> <span class="n">_engine</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">_engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span>
<span class="n">database_url</span><span class="p">,</span>
<span class="n">connect_args</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;check_same_thread&quot;</span><span class="p">:</span> <span class="kc">False</span><span class="p">}</span>
<span class="k">if</span> <span class="s2">&quot;sqlite&quot;</span> <span class="ow">in</span> <span class="n">database_url</span>
<span class="k">else</span> <span class="p">{},</span>
<span class="n">echo</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">_engine</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.get_or_create_user" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_or_create_user</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">full_name</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">first_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">last_name</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Get or create user by Telegram user ID.</p>
<p>On create, name fields come from Telegram. On update: username is always
synced; full_name, first_name, last_name are updated only if
name_manually_edited is False (otherwise existing display name is kept).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>full_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Display full name.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>username</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram username (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>first_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram first name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>last_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram last name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a></code>
</td>
<td>
<div class="doc-md-description">
<p>User instance (created or updated).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span>
<span class="normal">64</span>
<span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_or_create_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">username</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">first_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">last_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Get or create user by Telegram user ID.</span>
<span class="sd"> On create, name fields come from Telegram. On update: username is always</span>
<span class="sd"> synced; full_name, first_name, last_name are updated only if</span>
<span class="sd"> name_manually_edited is False (otherwise existing display name is kept).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> full_name: Display full name.</span>
<span class="sd"> username: Telegram username (optional).</span>
<span class="sd"> first_name: Telegram first name (optional).</span>
<span class="sd"> last_name: Telegram last name (optional).</span>
<span class="sd"> Returns:</span>
<span class="sd"> User instance (created or updated).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_user_by_telegram_id</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span><span class="p">:</span>
<span class="n">user</span><span class="o">.</span><span class="n">username</span> <span class="o">=</span> <span class="n">username</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">name_manually_edited</span><span class="p">:</span>
<span class="n">user</span><span class="o">.</span><span class="n">full_name</span> <span class="o">=</span> <span class="n">full_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">first_name</span> <span class="o">=</span> <span class="n">first_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">last_name</span> <span class="o">=</span> <span class="n">last_name</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="n">telegram_user_id</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="n">first_name</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="n">last_name</span><span class="p">,</span>
<span class="n">name_manually_edited</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.get_or_create_user_by_full_name" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_or_create_user_by_full_name</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">full_name</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Find user by exact full_name or create one (for duty-schedule import).</p>
<p>New users have telegram_user_id=None and name_manually_edited=True.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>full_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Exact full name to match or set.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a></code>
</td>
<td>
<div class="doc-md-description">
<p>User instance (existing or newly created).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_or_create_user_by_full_name</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Find user by exact full_name or create one (for duty-schedule import).</span>
<span class="sd"> New users have telegram_user_id=None and name_manually_edited=True.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> full_name: Exact full name to match or set.</span>
<span class="sd"> Returns:</span>
<span class="sd"> User instance (existing or newly created).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">full_name</span> <span class="o">==</span> <span class="n">full_name</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span> <span class="n">user</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">name_manually_edited</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.get_session" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_session</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Create a new session from the factory for the given URL.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_session</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Session</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Create a new session from the factory for the given URL.&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_session_factory</span><span class="p">(</span><span class="n">database_url</span><span class="p">)()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.get_session_factory" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_session_factory</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return cached session factory for the given URL (one per process).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_session_factory</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">sessionmaker</span><span class="p">[</span><span class="n">Session</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return cached session factory for the given URL (one per process).&quot;&quot;&quot;</span>
<span class="k">global</span> <span class="n">_SessionLocal</span>
<span class="k">if</span> <span class="n">_SessionLocal</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">engine</span> <span class="o">=</span> <span class="n">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span>
<span class="n">_SessionLocal</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">autocommit</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">autoflush</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
<span class="k">return</span> <span class="n">_SessionLocal</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.init_db" class="doc doc-heading">
<code class="highlight language-python"><span class="n">init_db</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Create all tables from SQLAlchemy metadata.</p>
<p>Prefer Alembic migrations for schema changes in production.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>database_url</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>SQLAlchemy database URL.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/__init__.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">init_db</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Create all tables from SQLAlchemy metadata.</span>
<span class="sd"> Prefer Alembic migrations for schema changes in production.</span>
<span class="sd"> Args:</span>
<span class="sd"> database_url: SQLAlchemy database URL.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">engine</span> <span class="o">=</span> <span class="n">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span>
<span class="n">Base</span><span class="o">.</span><span class="n">metadata</span><span class="o">.</span><span class="n">create_all</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.insert_duty" class="doc doc-heading">
<code class="highlight language-python"><span class="n">insert_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">start_at</span><span class="p">,</span> <span class="n">end_at</span><span class="p">,</span> <span class="n">event_type</span><span class="o">=</span><span class="s1">&#39;duty&#39;</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Create a duty record.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>User id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>start_at</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start time UTC, ISO 8601 with Z (e.g. 2025-01-15T09:00:00Z).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>end_at</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End time UTC, ISO 8601 with Z.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>event_type</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>One of "duty", "unavailable", "vacation". Default "duty".</p>
</div>
</td>
<td>
<code>&#39;duty&#39;</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a></code>
</td>
<td>
<div class="doc-md-description">
<p>Created Duty instance.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">288</span>
<span class="normal">289</span>
<span class="normal">290</span>
<span class="normal">291</span>
<span class="normal">292</span>
<span class="normal">293</span>
<span class="normal">294</span>
<span class="normal">295</span>
<span class="normal">296</span>
<span class="normal">297</span>
<span class="normal">298</span>
<span class="normal">299</span>
<span class="normal">300</span>
<span class="normal">301</span>
<span class="normal">302</span>
<span class="normal">303</span>
<span class="normal">304</span>
<span class="normal">305</span>
<span class="normal">306</span>
<span class="normal">307</span>
<span class="normal">308</span>
<span class="normal">309</span>
<span class="normal">310</span>
<span class="normal">311</span>
<span class="normal">312</span>
<span class="normal">313</span>
<span class="normal">314</span>
<span class="normal">315</span>
<span class="normal">316</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">insert_duty</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">start_at</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">end_at</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">event_type</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;duty&quot;</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Duty</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Create a duty record.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> user_id: User id.</span>
<span class="sd"> start_at: Start time UTC, ISO 8601 with Z (e.g. 2025-01-15T09:00:00Z).</span>
<span class="sd"> end_at: End time UTC, ISO 8601 with Z.</span>
<span class="sd"> event_type: One of &quot;duty&quot;, &quot;unavailable&quot;, &quot;vacation&quot;. Default &quot;duty&quot;.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Created Duty instance.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">duty</span> <span class="o">=</span> <span class="n">Duty</span><span class="p">(</span>
<span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">,</span>
<span class="n">start_at</span><span class="o">=</span><span class="n">start_at</span><span class="p">,</span>
<span class="n">end_at</span><span class="o">=</span><span class="n">end_at</span><span class="p">,</span>
<span class="n">event_type</span><span class="o">=</span><span class="n">event_type</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">duty</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">duty</span><span class="p">)</span>
<span class="k">return</span> <span class="n">duty</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.session_scope" class="doc doc-heading">
<code class="highlight language-python"><span class="n">session_scope</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Context manager that yields a session; rolls back on exception, closes on exit.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>database_url</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>SQLAlchemy database URL.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Yields:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Session instance. Caller must not use it after exit.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@contextmanager</span>
<span class="k">def</span><span class="w"> </span><span class="nf">session_scope</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Generator</span><span class="p">[</span><span class="n">Session</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Context manager that yields a session; rolls back on exception, closes on exit.</span>
<span class="sd"> Args:</span>
<span class="sd"> database_url: SQLAlchemy database URL.</span>
<span class="sd"> Yields:</span>
<span class="sd"> Session instance. Caller must not use it after exit.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">session</span> <span class="o">=</span> <span class="n">get_session</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">yield</span> <span class="n">session</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="n">session</span><span class="o">.</span><span class="n">rollback</span><span class="p">()</span>
<span class="k">raise</span>
<span class="k">finally</span><span class="p">:</span>
<span class="n">session</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.set_user_phone" class="doc doc-heading">
<code class="highlight language-python"><span class="n">set_user_phone</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">phone</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Set or clear phone for user by Telegram user id.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>phone</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Phone string or None to clear.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Updated User or None if not found.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">453</span>
<span class="normal">454</span>
<span class="normal">455</span>
<span class="normal">456</span>
<span class="normal">457</span>
<span class="normal">458</span>
<span class="normal">459</span>
<span class="normal">460</span>
<span class="normal">461</span>
<span class="normal">462</span>
<span class="normal">463</span>
<span class="normal">464</span>
<span class="normal">465</span>
<span class="normal">466</span>
<span class="normal">467</span>
<span class="normal">468</span>
<span class="normal">469</span>
<span class="normal">470</span>
<span class="normal">471</span>
<span class="normal">472</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">set_user_phone</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">phone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Set or clear phone for user by Telegram user id.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> phone: Phone string or None to clear.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Updated User or None if not found.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">telegram_user_id</span> <span class="o">==</span> <span class="n">telegram_user_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="n">user</span><span class="o">.</span><span class="n">phone</span> <span class="o">=</span> <span class="n">phone</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.update_user_display_name" class="doc doc-heading">
<code class="highlight language-python"><span class="n">update_user_display_name</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">full_name</span><span class="p">,</span> <span class="n">first_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">last_name</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Update display name and set name_manually_edited=True.</p>
<p>Use from API or admin when name is changed manually; subsequent
get_or_create_user will not overwrite these fields.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>full_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>New full name.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>first_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>New first name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>last_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>New last name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Updated User or None if not found.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span>
<span class="normal">129</span>
<span class="normal">130</span>
<span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">update_user_display_name</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">first_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">last_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Update display name and set name_manually_edited=True.</span>
<span class="sd"> Use from API or admin when name is changed manually; subsequent</span>
<span class="sd"> get_or_create_user will not overwrite these fields.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> full_name: New full name.</span>
<span class="sd"> first_name: New first name (optional).</span>
<span class="sd"> last_name: New last name (optional).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Updated User or None if not found.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">telegram_user_id</span> <span class="o">==</span> <span class="n">telegram_user_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="n">user</span><span class="o">.</span><span class="n">full_name</span> <span class="o">=</span> <span class="n">full_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">first_name</span> <span class="o">=</span> <span class="n">first_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">last_name</span> <span class="o">=</span> <span class="n">last_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">name_manually_edited</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.db.models" class="doc doc-heading">
<code>duty_teller.db.models</code>
</h2>
<div class="doc doc-contents first">
<p>SQLAlchemy ORM models for users and duties.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.models.Base" class="doc doc-heading">
<code>Base</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="sqlalchemy.orm.DeclarativeBase">DeclarativeBase</span></code></p>
<p>Declarative base for all models.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 7</span>
<span class="normal"> 8</span>
<span class="normal"> 9</span>
<span class="normal">10</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Base</span><span class="p">(</span><span class="n">DeclarativeBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Declarative base for all models.&quot;&quot;&quot;</span>
<span class="k">pass</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.models.CalendarSubscriptionToken" class="doc doc-heading">
<code>CalendarSubscriptionToken</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Base (duty_teller.db.models.Base)" href="#duty_teller.db.models.Base">Base</a></code></p>
<p>One active calendar subscription token per user; token_hash is unique.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">CalendarSubscriptionToken</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;One active calendar subscription token per user; token_hash is unique.&quot;&quot;&quot;</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">&quot;calendar_subscription_tokens&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">user_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">Integer</span><span class="p">,</span> <span class="n">ForeignKey</span><span class="p">(</span><span class="s2">&quot;users.id&quot;</span><span class="p">),</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
<span class="n">token_hash</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">created_at</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.models.Duty" class="doc doc-heading">
<code>Duty</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Base (duty_teller.db.models.Base)" href="#duty_teller.db.models.Base">Base</a></code></p>
<p>Single duty/unavailable/vacation slot (UTC start_at/end_at, event_type).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">Duty</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Single duty/unavailable/vacation slot (UTC start_at/end_at, event_type).&quot;&quot;&quot;</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">&quot;duties&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">user_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">Integer</span><span class="p">,</span> <span class="n">ForeignKey</span><span class="p">(</span><span class="s2">&quot;users.id&quot;</span><span class="p">),</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
<span class="c1"># UTC, ISO 8601 with Z suffix (e.g. 2025-01-15T09:00:00Z)</span>
<span class="n">start_at</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">end_at</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="c1"># duty | unavailable | vacation</span>
<span class="n">event_type</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">server_default</span><span class="o">=</span><span class="s2">&quot;duty&quot;</span><span class="p">)</span>
<span class="n">user</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="s2">&quot;User&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="s2">&quot;User&quot;</span><span class="p">,</span> <span class="n">back_populates</span><span class="o">=</span><span class="s2">&quot;duties&quot;</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.models.GroupDutyPin" class="doc doc-heading">
<code>GroupDutyPin</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Base (duty_teller.db.models.Base)" href="#duty_teller.db.models.Base">Base</a></code></p>
<p>Stores which message to update in each group for the pinned duty notice.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">GroupDutyPin</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Stores which message to update in each group for the pinned duty notice.&quot;&quot;&quot;</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">&quot;group_duty_pins&quot;</span>
<span class="n">chat_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">BigInteger</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">message_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.models.User" class="doc doc-heading">
<code>User</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="Base (duty_teller.db.models.Base)" href="#duty_teller.db.models.Base">Base</a></code></p>
<p>Telegram user and display name; may have telegram_user_id=None for import-only users.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/models.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">13</span>
<span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">User</span><span class="p">(</span><span class="n">Base</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Telegram user and display name; may have telegram_user_id=None for import-only users.&quot;&quot;&quot;</span>
<span class="n">__tablename__</span> <span class="o">=</span> <span class="s2">&quot;users&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Integer</span><span class="p">,</span> <span class="n">primary_key</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">autoincrement</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">BigInteger</span><span class="p">,</span> <span class="n">unique</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span>
<span class="p">)</span>
<span class="n">full_name</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">username</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">first_name</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">last_name</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">phone</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span><span class="n">Text</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">name_manually_edited</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="n">mapped_column</span><span class="p">(</span>
<span class="n">Boolean</span><span class="p">,</span> <span class="n">nullable</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">server_default</span><span class="o">=</span><span class="s2">&quot;0&quot;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="kc">False</span>
<span class="p">)</span>
<span class="n">duties</span><span class="p">:</span> <span class="n">Mapped</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="s2">&quot;Duty&quot;</span><span class="p">]]</span> <span class="o">=</span> <span class="n">relationship</span><span class="p">(</span><span class="s2">&quot;Duty&quot;</span><span class="p">,</span> <span class="n">back_populates</span><span class="o">=</span><span class="s2">&quot;user&quot;</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.db.schemas" class="doc doc-heading">
<code>duty_teller.db.schemas</code>
</h2>
<div class="doc doc-contents first">
<p>Pydantic schemas for API request/response and validation.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.CalendarEvent" class="doc doc-heading">
<code>CalendarEvent</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="pydantic.BaseModel">BaseModel</span></code></p>
<p>External calendar event (e.g. holiday) for a single day.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">CalendarEvent</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;External calendar event (e.g. holiday) for a single day.&quot;&quot;&quot;</span>
<span class="n">date</span><span class="p">:</span> <span class="nb">str</span> <span class="c1"># YYYY-MM-DD</span>
<span class="n">summary</span><span class="p">:</span> <span class="nb">str</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.DutyBase" class="doc doc-heading">
<code>DutyBase</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="pydantic.BaseModel">BaseModel</span></code></p>
<p>Duty fields: user_id, start_at, end_at (UTC ISO 8601 with Z).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyBase</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty fields: user_id, start_at, end_at (UTC ISO 8601 with Z).&quot;&quot;&quot;</span>
<span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">start_at</span><span class="p">:</span> <span class="nb">str</span> <span class="c1"># UTC, ISO 8601 with Z</span>
<span class="n">end_at</span><span class="p">:</span> <span class="nb">str</span> <span class="c1"># UTC, ISO 8601 with Z</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.DutyCreate" class="doc doc-heading">
<code>DutyCreate</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="DutyBase (duty_teller.db.schemas.DutyBase)" href="#duty_teller.db.schemas.DutyBase">DutyBase</a></code></p>
<p>Duty creation payload.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyCreate</span><span class="p">(</span><span class="n">DutyBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty creation payload.&quot;&quot;&quot;</span>
<span class="k">pass</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.DutyInDb" class="doc doc-heading">
<code>DutyInDb</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="DutyBase (duty_teller.db.schemas.DutyBase)" href="#duty_teller.db.schemas.DutyBase">DutyBase</a></code></p>
<p>Duty as stored in DB (includes id).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyInDb</span><span class="p">(</span><span class="n">DutyBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty as stored in DB (includes id).&quot;&quot;&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">from_attributes</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.DutyWithUser" class="doc doc-heading">
<code>DutyWithUser</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="DutyInDb (duty_teller.db.schemas.DutyInDb)" href="#duty_teller.db.schemas.DutyInDb">DutyInDb</a></code></p>
<p>Duty with full_name and event_type for calendar display.</p>
<p>event_type: only these values are returned; unknown DB values are mapped to "duty" in the API.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyWithUser</span><span class="p">(</span><span class="n">DutyInDb</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Duty with full_name and event_type for calendar display.</span>
<span class="sd"> event_type: only these values are returned; unknown DB values are mapped to &quot;duty&quot; in the API.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">event_type</span><span class="p">:</span> <span class="n">Literal</span><span class="p">[</span><span class="s2">&quot;duty&quot;</span><span class="p">,</span> <span class="s2">&quot;unavailable&quot;</span><span class="p">,</span> <span class="s2">&quot;vacation&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&quot;duty&quot;</span>
<span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">from_attributes</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.UserBase" class="doc doc-heading">
<code>UserBase</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="pydantic.BaseModel">BaseModel</span></code></p>
<p>Base user fields (full_name, username, first/last name).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 8</span>
<span class="normal"> 9</span>
<span class="normal">10</span>
<span class="normal">11</span>
<span class="normal">12</span>
<span class="normal">13</span>
<span class="normal">14</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">UserBase</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Base user fields (full_name, username, first/last name).&quot;&quot;&quot;</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">username</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">first_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">last_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.UserCreate" class="doc doc-heading">
<code>UserCreate</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="UserBase (duty_teller.db.schemas.UserBase)" href="#duty_teller.db.schemas.UserBase">UserBase</a></code></p>
<p>User creation payload including Telegram user id.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">UserCreate</span><span class="p">(</span><span class="n">UserBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;User creation payload including Telegram user id.&quot;&quot;&quot;</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.db.schemas.UserInDb" class="doc doc-heading">
<code>UserInDb</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><a class="autorefs autorefs-internal" title="UserBase (duty_teller.db.schemas.UserBase)" href="#duty_teller.db.schemas.UserBase">UserBase</a></code></p>
<p>User as stored in DB (includes id and telegram_user_id).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/schemas.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">UserInDb</span><span class="p">(</span><span class="n">UserBase</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;User as stored in DB (includes id and telegram_user_id).&quot;&quot;&quot;</span>
<span class="nb">id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span>
<span class="n">model_config</span> <span class="o">=</span> <span class="n">ConfigDict</span><span class="p">(</span><span class="n">from_attributes</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.db.session" class="doc doc-heading">
<code>duty_teller.db.session</code>
</h2>
<div class="doc doc-contents first">
<p>SQLAlchemy engine and session factory.</p>
<p>Engine and session factory are cached globally per process. Only one DATABASE_URL
is effectively used for the process lifetime. Using a different URL later (e.g. in
tests with in-memory SQLite) would still use the first engine. To use a different
URL in tests, set env (e.g. DATABASE_URL) before the first import of this module, or
clear _engine and _SessionLocal in test fixtures. Prefer session_scope() for all
callers so sessions are always closed and rolled back on error.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.session.get_engine" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return cached SQLAlchemy engine for the given URL (one per process).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return cached SQLAlchemy engine for the given URL (one per process).&quot;&quot;&quot;</span>
<span class="k">global</span> <span class="n">_engine</span>
<span class="k">if</span> <span class="n">_engine</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">_engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span>
<span class="n">database_url</span><span class="p">,</span>
<span class="n">connect_args</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;check_same_thread&quot;</span><span class="p">:</span> <span class="kc">False</span><span class="p">}</span>
<span class="k">if</span> <span class="s2">&quot;sqlite&quot;</span> <span class="ow">in</span> <span class="n">database_url</span>
<span class="k">else</span> <span class="p">{},</span>
<span class="n">echo</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">return</span> <span class="n">_engine</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.session.get_session" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_session</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Create a new session from the factory for the given URL.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_session</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Session</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Create a new session from the factory for the given URL.&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_session_factory</span><span class="p">(</span><span class="n">database_url</span><span class="p">)()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.session.get_session_factory" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_session_factory</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return cached session factory for the given URL (one per process).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_session_factory</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">sessionmaker</span><span class="p">[</span><span class="n">Session</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return cached session factory for the given URL (one per process).&quot;&quot;&quot;</span>
<span class="k">global</span> <span class="n">_SessionLocal</span>
<span class="k">if</span> <span class="n">_SessionLocal</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">engine</span> <span class="o">=</span> <span class="n">get_engine</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span>
<span class="n">_SessionLocal</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">autocommit</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">autoflush</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
<span class="k">return</span> <span class="n">_SessionLocal</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.session.session_scope" class="doc doc-heading">
<code class="highlight language-python"><span class="n">session_scope</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Context manager that yields a session; rolls back on exception, closes on exit.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>database_url</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>SQLAlchemy database URL.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Yields:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Session instance. Caller must not use it after exit.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/session.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@contextmanager</span>
<span class="k">def</span><span class="w"> </span><span class="nf">session_scope</span><span class="p">(</span><span class="n">database_url</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Generator</span><span class="p">[</span><span class="n">Session</span><span class="p">,</span> <span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Context manager that yields a session; rolls back on exception, closes on exit.</span>
<span class="sd"> Args:</span>
<span class="sd"> database_url: SQLAlchemy database URL.</span>
<span class="sd"> Yields:</span>
<span class="sd"> Session instance. Caller must not use it after exit.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">session</span> <span class="o">=</span> <span class="n">get_session</span><span class="p">(</span><span class="n">database_url</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">yield</span> <span class="n">session</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="n">session</span><span class="o">.</span><span class="n">rollback</span><span class="p">()</span>
<span class="k">raise</span>
<span class="k">finally</span><span class="p">:</span>
<span class="n">session</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.db.repository" class="doc doc-heading">
<code>duty_teller.db.repository</code>
</h2>
<div class="doc doc-contents first">
<p>Repository: get_or_create_user, get_duties, insert_duty, get_current_duty, group_duty_pins.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.create_calendar_token" class="doc doc-heading">
<code class="highlight language-python"><span class="n">create_calendar_token</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Create a new calendar subscription token for the user.</p>
<p>Any existing tokens for this user are removed. The raw token is returned
only once (not stored in plain text).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>User id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Raw token string (e.g. for URL /api/calendar/ical/{token}.ics).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">232</span>
<span class="normal">233</span>
<span class="normal">234</span>
<span class="normal">235</span>
<span class="normal">236</span>
<span class="normal">237</span>
<span class="normal">238</span>
<span class="normal">239</span>
<span class="normal">240</span>
<span class="normal">241</span>
<span class="normal">242</span>
<span class="normal">243</span>
<span class="normal">244</span>
<span class="normal">245</span>
<span class="normal">246</span>
<span class="normal">247</span>
<span class="normal">248</span>
<span class="normal">249</span>
<span class="normal">250</span>
<span class="normal">251</span>
<span class="normal">252</span>
<span class="normal">253</span>
<span class="normal">254</span>
<span class="normal">255</span>
<span class="normal">256</span>
<span class="normal">257</span>
<span class="normal">258</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">create_calendar_token</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Create a new calendar subscription token for the user.</span>
<span class="sd"> Any existing tokens for this user are removed. The raw token is returned</span>
<span class="sd"> only once (not stored in plain text).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> user_id: User id.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Raw token string (e.g. for URL /api/calendar/ical/{token}.ics).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">CalendarSubscriptionToken</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">CalendarSubscriptionToken</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">user_id</span>
<span class="p">)</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">synchronize_session</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">raw_token</span> <span class="o">=</span> <span class="n">secrets</span><span class="o">.</span><span class="n">token_urlsafe</span><span class="p">(</span><span class="mi">32</span><span class="p">)</span>
<span class="n">token_hash_val</span> <span class="o">=</span> <span class="n">_token_hash</span><span class="p">(</span><span class="n">raw_token</span><span class="p">)</span>
<span class="n">now_iso</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">T%H:%M:%SZ&quot;</span><span class="p">)</span>
<span class="n">record</span> <span class="o">=</span> <span class="n">CalendarSubscriptionToken</span><span class="p">(</span>
<span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">,</span>
<span class="n">token_hash</span><span class="o">=</span><span class="n">token_hash_val</span><span class="p">,</span>
<span class="n">created_at</span><span class="o">=</span><span class="n">now_iso</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">record</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">return</span> <span class="n">raw_token</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.delete_duties_in_range" class="doc doc-heading">
<code class="highlight language-python"><span class="n">delete_duties_in_range</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Delete all duties of the user that overlap the given date range.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>User id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Number of duties deleted.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">138</span>
<span class="normal">139</span>
<span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span>
<span class="normal">143</span>
<span class="normal">144</span>
<span class="normal">145</span>
<span class="normal">146</span>
<span class="normal">147</span>
<span class="normal">148</span>
<span class="normal">149</span>
<span class="normal">150</span>
<span class="normal">151</span>
<span class="normal">152</span>
<span class="normal">153</span>
<span class="normal">154</span>
<span class="normal">155</span>
<span class="normal">156</span>
<span class="normal">157</span>
<span class="normal">158</span>
<span class="normal">159</span>
<span class="normal">160</span>
<span class="normal">161</span>
<span class="normal">162</span>
<span class="normal">163</span>
<span class="normal">164</span>
<span class="normal">165</span>
<span class="normal">166</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">delete_duties_in_range</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Delete all duties of the user that overlap the given date range.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> user_id: User id.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Number of duties deleted.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">to_next</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">to_date</span> <span class="o">+</span> <span class="s2">&quot;T00:00:00&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">q</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">user_id</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;</span> <span class="n">to_next</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;=</span> <span class="n">from_date</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">count</span> <span class="o">=</span> <span class="n">q</span><span class="o">.</span><span class="n">count</span><span class="p">()</span>
<span class="n">q</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">synchronize_session</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="k">return</span> <span class="n">count</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.delete_group_duty_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">delete_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Remove the pinned duty message record for the chat (e.g. when bot leaves group).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">427</span>
<span class="normal">428</span>
<span class="normal">429</span>
<span class="normal">430</span>
<span class="normal">431</span>
<span class="normal">432</span>
<span class="normal">433</span>
<span class="normal">434</span>
<span class="normal">435</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">delete_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Remove the pinned duty message record for the chat (e.g. when bot leaves group).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="o">.</span><span class="n">chat_id</span> <span class="o">==</span> <span class="n">chat_id</span><span class="p">)</span><span class="o">.</span><span class="n">delete</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_all_group_duty_pin_chat_ids" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_all_group_duty_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return all chat_ids that have a pinned duty message.</p>
<p>Used to restore update jobs on bot startup.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="int">int</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of chat ids.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">438</span>
<span class="normal">439</span>
<span class="normal">440</span>
<span class="normal">441</span>
<span class="normal">442</span>
<span class="normal">443</span>
<span class="normal">444</span>
<span class="normal">445</span>
<span class="normal">446</span>
<span class="normal">447</span>
<span class="normal">448</span>
<span class="normal">449</span>
<span class="normal">450</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_all_group_duty_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return all chat_ids that have a pinned duty message.</span>
<span class="sd"> Used to restore update jobs on bot startup.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of chat ids.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">rows</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="o">.</span><span class="n">chat_id</span><span class="p">)</span><span class="o">.</span><span class="n">all</span><span class="p">()</span>
<span class="k">return</span> <span class="p">[</span><span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">]</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_current_duty" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_current_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">at_utc</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return the duty and user active at the given UTC time (event_type='duty').</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>at_utc</code>
</td>
<td>
<code><span title="datetime.datetime">datetime</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Point in time (timezone-aware or naive UTC).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="tuple">tuple</span>[<a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a>, <a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a>] | None</code>
</td>
<td>
<div class="doc-md-description">
<p>(Duty, User) or None if no duty at that time.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">319</span>
<span class="normal">320</span>
<span class="normal">321</span>
<span class="normal">322</span>
<span class="normal">323</span>
<span class="normal">324</span>
<span class="normal">325</span>
<span class="normal">326</span>
<span class="normal">327</span>
<span class="normal">328</span>
<span class="normal">329</span>
<span class="normal">330</span>
<span class="normal">331</span>
<span class="normal">332</span>
<span class="normal">333</span>
<span class="normal">334</span>
<span class="normal">335</span>
<span class="normal">336</span>
<span class="normal">337</span>
<span class="normal">338</span>
<span class="normal">339</span>
<span class="normal">340</span>
<span class="normal">341</span>
<span class="normal">342</span>
<span class="normal">343</span>
<span class="normal">344</span>
<span class="normal">345</span>
<span class="normal">346</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_current_duty</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">at_utc</span><span class="p">:</span> <span class="n">datetime</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="n">Duty</span><span class="p">,</span> <span class="n">User</span><span class="p">]</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return the duty and user active at the given UTC time (event_type=&#39;duty&#39;).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> at_utc: Point in time (timezone-aware or naive UTC).</span>
<span class="sd"> Returns:</span>
<span class="sd"> (Duty, User) or None if no duty at that time.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">timezone</span>
<span class="k">if</span> <span class="n">at_utc</span><span class="o">.</span><span class="n">tzinfo</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">at_utc</span> <span class="o">=</span> <span class="n">at_utc</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>
<span class="n">now_iso</span> <span class="o">=</span> <span class="n">at_utc</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">T%H:%M:%S&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&quot;Z&quot;</span>
<span class="n">row</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">,</span> <span class="n">User</span><span class="p">)</span>
<span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">User</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">event_type</span> <span class="o">==</span> <span class="s2">&quot;duty&quot;</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;=</span> <span class="n">now_iso</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;</span> <span class="n">now_iso</span><span class="p">,</span>
<span class="p">)</span>
<span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">row</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">return</span> <span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_duties" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_duties</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return duties overlapping the given date range with user full_name.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="tuple">tuple</span>[<a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a>, <span title="str">str</span>]]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of (Duty, full_name) tuples.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">169</span>
<span class="normal">170</span>
<span class="normal">171</span>
<span class="normal">172</span>
<span class="normal">173</span>
<span class="normal">174</span>
<span class="normal">175</span>
<span class="normal">176</span>
<span class="normal">177</span>
<span class="normal">178</span>
<span class="normal">179</span>
<span class="normal">180</span>
<span class="normal">181</span>
<span class="normal">182</span>
<span class="normal">183</span>
<span class="normal">184</span>
<span class="normal">185</span>
<span class="normal">186</span>
<span class="normal">187</span>
<span class="normal">188</span>
<span class="normal">189</span>
<span class="normal">190</span>
<span class="normal">191</span>
<span class="normal">192</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_duties</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="n">Duty</span><span class="p">,</span> <span class="nb">str</span><span class="p">]]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return duties overlapping the given date range with user full_name.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of (Duty, full_name) tuples.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">to_date_next</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">to_date</span> <span class="o">+</span> <span class="s2">&quot;T00:00:00&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">q</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">,</span> <span class="n">User</span><span class="o">.</span><span class="n">full_name</span><span class="p">)</span>
<span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">User</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;</span> <span class="n">to_date_next</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;=</span> <span class="n">from_date</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">all</span><span class="p">())</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_duties_for_user" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_duties_for_user</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">from_date</span><span class="p">,</span> <span class="n">to_date</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return duties for one user overlapping the date range.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>User id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>from_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>to_date</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End date YYYY-MM-DD.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="tuple">tuple</span>[<a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a>, <span title="str">str</span>]]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of (Duty, full_name) tuples.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">195</span>
<span class="normal">196</span>
<span class="normal">197</span>
<span class="normal">198</span>
<span class="normal">199</span>
<span class="normal">200</span>
<span class="normal">201</span>
<span class="normal">202</span>
<span class="normal">203</span>
<span class="normal">204</span>
<span class="normal">205</span>
<span class="normal">206</span>
<span class="normal">207</span>
<span class="normal">208</span>
<span class="normal">209</span>
<span class="normal">210</span>
<span class="normal">211</span>
<span class="normal">212</span>
<span class="normal">213</span>
<span class="normal">214</span>
<span class="normal">215</span>
<span class="normal">216</span>
<span class="normal">217</span>
<span class="normal">218</span>
<span class="normal">219</span>
<span class="normal">220</span>
<span class="normal">221</span>
<span class="normal">222</span>
<span class="normal">223</span>
<span class="normal">224</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_duties_for_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">from_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">to_date</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">tuple</span><span class="p">[</span><span class="n">Duty</span><span class="p">,</span> <span class="nb">str</span><span class="p">]]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return duties for one user overlapping the date range.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> user_id: User id.</span>
<span class="sd"> from_date: Start date YYYY-MM-DD.</span>
<span class="sd"> to_date: End date YYYY-MM-DD.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of (Duty, full_name) tuples.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">to_date_next</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">to_date</span> <span class="o">+</span> <span class="s2">&quot;T00:00:00&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">q</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">,</span> <span class="n">User</span><span class="o">.</span><span class="n">full_name</span><span class="p">)</span>
<span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">User</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">user_id</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;</span> <span class="n">to_date_next</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;=</span> <span class="n">from_date</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">q</span><span class="o">.</span><span class="n">all</span><span class="p">())</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_group_duty_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Get the pinned duty message record for a chat.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="GroupDutyPin (duty_teller.db.models.GroupDutyPin)" href="#duty_teller.db.models.GroupDutyPin">GroupDutyPin</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>GroupDutyPin or None.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">390</span>
<span class="normal">391</span>
<span class="normal">392</span>
<span class="normal">393</span>
<span class="normal">394</span>
<span class="normal">395</span>
<span class="normal">396</span>
<span class="normal">397</span>
<span class="normal">398</span>
<span class="normal">399</span>
<span class="normal">400</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">GroupDutyPin</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Get the pinned duty message record for a chat.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> Returns:</span>
<span class="sd"> GroupDutyPin or None.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="o">.</span><span class="n">chat_id</span> <span class="o">==</span> <span class="n">chat_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_next_shift_end" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_next_shift_end</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">after_utc</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return the end_at of the current or next duty (event_type='duty').</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>after_utc</code>
</td>
<td>
<code><span title="datetime.datetime">datetime</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Point in time (timezone-aware or naive UTC).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="datetime.datetime">datetime</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>End datetime (naive UTC) or None if no current or future duty.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">349</span>
<span class="normal">350</span>
<span class="normal">351</span>
<span class="normal">352</span>
<span class="normal">353</span>
<span class="normal">354</span>
<span class="normal">355</span>
<span class="normal">356</span>
<span class="normal">357</span>
<span class="normal">358</span>
<span class="normal">359</span>
<span class="normal">360</span>
<span class="normal">361</span>
<span class="normal">362</span>
<span class="normal">363</span>
<span class="normal">364</span>
<span class="normal">365</span>
<span class="normal">366</span>
<span class="normal">367</span>
<span class="normal">368</span>
<span class="normal">369</span>
<span class="normal">370</span>
<span class="normal">371</span>
<span class="normal">372</span>
<span class="normal">373</span>
<span class="normal">374</span>
<span class="normal">375</span>
<span class="normal">376</span>
<span class="normal">377</span>
<span class="normal">378</span>
<span class="normal">379</span>
<span class="normal">380</span>
<span class="normal">381</span>
<span class="normal">382</span>
<span class="normal">383</span>
<span class="normal">384</span>
<span class="normal">385</span>
<span class="normal">386</span>
<span class="normal">387</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_next_shift_end</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">after_utc</span><span class="p">:</span> <span class="n">datetime</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">datetime</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return the end_at of the current or next duty (event_type=&#39;duty&#39;).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> after_utc: Point in time (timezone-aware or naive UTC).</span>
<span class="sd"> Returns:</span>
<span class="sd"> End datetime (naive UTC) or None if no current or future duty.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="kn">from</span><span class="w"> </span><span class="nn">datetime</span><span class="w"> </span><span class="kn">import</span> <span class="n">timezone</span>
<span class="k">if</span> <span class="n">after_utc</span><span class="o">.</span><span class="n">tzinfo</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">after_utc</span> <span class="o">=</span> <span class="n">after_utc</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>
<span class="n">after_iso</span> <span class="o">=</span> <span class="n">after_utc</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y-%m-</span><span class="si">%d</span><span class="s2">T%H:%M:%S&quot;</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&quot;Z&quot;</span>
<span class="n">current</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">event_type</span> <span class="o">==</span> <span class="s2">&quot;duty&quot;</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&lt;=</span> <span class="n">after_iso</span><span class="p">,</span>
<span class="n">Duty</span><span class="o">.</span><span class="n">end_at</span> <span class="o">&gt;</span> <span class="n">after_iso</span><span class="p">,</span>
<span class="p">)</span>
<span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">current</span><span class="p">:</span>
<span class="k">return</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">current</span><span class="o">.</span><span class="n">end_at</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;Z&quot;</span><span class="p">,</span> <span class="s2">&quot;+00:00&quot;</span><span class="p">))</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span>
<span class="n">tzinfo</span><span class="o">=</span><span class="kc">None</span>
<span class="p">)</span>
<span class="n">next_duty</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">Duty</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">Duty</span><span class="o">.</span><span class="n">event_type</span> <span class="o">==</span> <span class="s2">&quot;duty&quot;</span><span class="p">,</span> <span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span> <span class="o">&gt;</span> <span class="n">after_iso</span><span class="p">)</span>
<span class="o">.</span><span class="n">order_by</span><span class="p">(</span><span class="n">Duty</span><span class="o">.</span><span class="n">start_at</span><span class="p">)</span>
<span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">next_duty</span><span class="p">:</span>
<span class="k">return</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">next_duty</span><span class="o">.</span><span class="n">end_at</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;Z&quot;</span><span class="p">,</span> <span class="s2">&quot;+00:00&quot;</span><span class="p">))</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span>
<span class="n">tzinfo</span><span class="o">=</span><span class="kc">None</span>
<span class="p">)</span>
<span class="k">return</span> <span class="kc">None</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_or_create_user" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_or_create_user</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">full_name</span><span class="p">,</span> <span class="n">username</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">first_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">last_name</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Get or create user by Telegram user ID.</p>
<p>On create, name fields come from Telegram. On update: username is always
synced; full_name, first_name, last_name are updated only if
name_manually_edited is False (otherwise existing display name is kept).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>full_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Display full name.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>username</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram username (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>first_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram first name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>last_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram last name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a></code>
</td>
<td>
<div class="doc-md-description">
<p>User instance (created or updated).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span>
<span class="normal">64</span>
<span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_or_create_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">username</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">first_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">last_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Get or create user by Telegram user ID.</span>
<span class="sd"> On create, name fields come from Telegram. On update: username is always</span>
<span class="sd"> synced; full_name, first_name, last_name are updated only if</span>
<span class="sd"> name_manually_edited is False (otherwise existing display name is kept).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> full_name: Display full name.</span>
<span class="sd"> username: Telegram username (optional).</span>
<span class="sd"> first_name: Telegram first name (optional).</span>
<span class="sd"> last_name: Telegram last name (optional).</span>
<span class="sd"> Returns:</span>
<span class="sd"> User instance (created or updated).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_user_by_telegram_id</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span><span class="p">:</span>
<span class="n">user</span><span class="o">.</span><span class="n">username</span> <span class="o">=</span> <span class="n">username</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">name_manually_edited</span><span class="p">:</span>
<span class="n">user</span><span class="o">.</span><span class="n">full_name</span> <span class="o">=</span> <span class="n">full_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">first_name</span> <span class="o">=</span> <span class="n">first_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">last_name</span> <span class="o">=</span> <span class="n">last_name</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="n">telegram_user_id</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="n">first_name</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="n">last_name</span><span class="p">,</span>
<span class="n">name_manually_edited</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_or_create_user_by_full_name" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_or_create_user_by_full_name</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">full_name</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Find user by exact full_name or create one (for duty-schedule import).</p>
<p>New users have telegram_user_id=None and name_manually_edited=True.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>full_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Exact full name to match or set.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a></code>
</td>
<td>
<div class="doc-md-description">
<p>User instance (existing or newly created).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_or_create_user_by_full_name</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Find user by exact full_name or create one (for duty-schedule import).</span>
<span class="sd"> New users have telegram_user_id=None and name_manually_edited=True.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> full_name: Exact full name to match or set.</span>
<span class="sd"> Returns:</span>
<span class="sd"> User instance (existing or newly created).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">full_name</span> <span class="o">==</span> <span class="n">full_name</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span> <span class="n">user</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">User</span><span class="p">(</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">name_manually_edited</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_user_by_calendar_token" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_user_by_calendar_token</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">token</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Find user by calendar subscription token.</p>
<p>Uses constant-time comparison to avoid timing leaks.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>token</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Raw token from URL.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>User or None if token is invalid or not found.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">261</span>
<span class="normal">262</span>
<span class="normal">263</span>
<span class="normal">264</span>
<span class="normal">265</span>
<span class="normal">266</span>
<span class="normal">267</span>
<span class="normal">268</span>
<span class="normal">269</span>
<span class="normal">270</span>
<span class="normal">271</span>
<span class="normal">272</span>
<span class="normal">273</span>
<span class="normal">274</span>
<span class="normal">275</span>
<span class="normal">276</span>
<span class="normal">277</span>
<span class="normal">278</span>
<span class="normal">279</span>
<span class="normal">280</span>
<span class="normal">281</span>
<span class="normal">282</span>
<span class="normal">283</span>
<span class="normal">284</span>
<span class="normal">285</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_user_by_calendar_token</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">token</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Find user by calendar subscription token.</span>
<span class="sd"> Uses constant-time comparison to avoid timing leaks.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> token: Raw token from URL.</span>
<span class="sd"> Returns:</span>
<span class="sd"> User or None if token is invalid or not found.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">token_hash_val</span> <span class="o">=</span> <span class="n">_token_hash</span><span class="p">(</span><span class="n">token</span><span class="p">)</span>
<span class="n">row</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">CalendarSubscriptionToken</span><span class="p">,</span> <span class="n">User</span><span class="p">)</span>
<span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">CalendarSubscriptionToken</span><span class="o">.</span><span class="n">user_id</span> <span class="o">==</span> <span class="n">User</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">CalendarSubscriptionToken</span><span class="o">.</span><span class="n">token_hash</span> <span class="o">==</span> <span class="n">token_hash_val</span><span class="p">)</span>
<span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">row</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="c1"># Constant-time compare to avoid timing leaks (token_hash is already hashed).</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">hmac</span><span class="o">.</span><span class="n">compare_digest</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="o">.</span><span class="n">token_hash</span><span class="p">,</span> <span class="n">token_hash_val</span><span class="p">):</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">return</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.get_user_by_telegram_id" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_user_by_telegram_id</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Find user by Telegram user ID.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>User or None if not found. Does not create a user.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">13</span>
<span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_user_by_telegram_id</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Find user by Telegram user ID.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> Returns:</span>
<span class="sd"> User or None if not found. Does not create a user.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">telegram_user_id</span> <span class="o">==</span> <span class="n">telegram_user_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.insert_duty" class="doc doc-heading">
<code class="highlight language-python"><span class="n">insert_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user_id</span><span class="p">,</span> <span class="n">start_at</span><span class="p">,</span> <span class="n">end_at</span><span class="p">,</span> <span class="n">event_type</span><span class="o">=</span><span class="s1">&#39;duty&#39;</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Create a duty record.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>User id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>start_at</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Start time UTC, ISO 8601 with Z (e.g. 2025-01-15T09:00:00Z).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>end_at</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>End time UTC, ISO 8601 with Z.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>event_type</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>One of "duty", "unavailable", "vacation". Default "duty".</p>
</div>
</td>
<td>
<code>&#39;duty&#39;</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="Duty (duty_teller.db.models.Duty)" href="#duty_teller.db.models.Duty">Duty</a></code>
</td>
<td>
<div class="doc-md-description">
<p>Created Duty instance.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">288</span>
<span class="normal">289</span>
<span class="normal">290</span>
<span class="normal">291</span>
<span class="normal">292</span>
<span class="normal">293</span>
<span class="normal">294</span>
<span class="normal">295</span>
<span class="normal">296</span>
<span class="normal">297</span>
<span class="normal">298</span>
<span class="normal">299</span>
<span class="normal">300</span>
<span class="normal">301</span>
<span class="normal">302</span>
<span class="normal">303</span>
<span class="normal">304</span>
<span class="normal">305</span>
<span class="normal">306</span>
<span class="normal">307</span>
<span class="normal">308</span>
<span class="normal">309</span>
<span class="normal">310</span>
<span class="normal">311</span>
<span class="normal">312</span>
<span class="normal">313</span>
<span class="normal">314</span>
<span class="normal">315</span>
<span class="normal">316</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">insert_duty</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">start_at</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">end_at</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">event_type</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;duty&quot;</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">Duty</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Create a duty record.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> user_id: User id.</span>
<span class="sd"> start_at: Start time UTC, ISO 8601 with Z (e.g. 2025-01-15T09:00:00Z).</span>
<span class="sd"> end_at: End time UTC, ISO 8601 with Z.</span>
<span class="sd"> event_type: One of &quot;duty&quot;, &quot;unavailable&quot;, &quot;vacation&quot;. Default &quot;duty&quot;.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Created Duty instance.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">duty</span> <span class="o">=</span> <span class="n">Duty</span><span class="p">(</span>
<span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">,</span>
<span class="n">start_at</span><span class="o">=</span><span class="n">start_at</span><span class="p">,</span>
<span class="n">end_at</span><span class="o">=</span><span class="n">end_at</span><span class="p">,</span>
<span class="n">event_type</span><span class="o">=</span><span class="n">event_type</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">duty</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">duty</span><span class="p">)</span>
<span class="k">return</span> <span class="n">duty</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.save_group_duty_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">save_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">message_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Save or update the pinned duty message for a chat.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>message_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Message id to pin/update.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="GroupDutyPin (duty_teller.db.models.GroupDutyPin)" href="#duty_teller.db.models.GroupDutyPin">GroupDutyPin</a></code>
</td>
<td>
<div class="doc-md-description">
<p>GroupDutyPin instance (created or updated).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">403</span>
<span class="normal">404</span>
<span class="normal">405</span>
<span class="normal">406</span>
<span class="normal">407</span>
<span class="normal">408</span>
<span class="normal">409</span>
<span class="normal">410</span>
<span class="normal">411</span>
<span class="normal">412</span>
<span class="normal">413</span>
<span class="normal">414</span>
<span class="normal">415</span>
<span class="normal">416</span>
<span class="normal">417</span>
<span class="normal">418</span>
<span class="normal">419</span>
<span class="normal">420</span>
<span class="normal">421</span>
<span class="normal">422</span>
<span class="normal">423</span>
<span class="normal">424</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">save_group_duty_pin</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">message_id</span><span class="p">:</span> <span class="nb">int</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">GroupDutyPin</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Save or update the pinned duty message for a chat.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> message_id: Message id to pin/update.</span>
<span class="sd"> Returns:</span>
<span class="sd"> GroupDutyPin instance (created or updated).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">pin</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">GroupDutyPin</span><span class="o">.</span><span class="n">chat_id</span> <span class="o">==</span> <span class="n">chat_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="n">pin</span><span class="p">:</span>
<span class="n">pin</span><span class="o">.</span><span class="n">message_id</span> <span class="o">=</span> <span class="n">message_id</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">pin</span> <span class="o">=</span> <span class="n">GroupDutyPin</span><span class="p">(</span><span class="n">chat_id</span><span class="o">=</span><span class="n">chat_id</span><span class="p">,</span> <span class="n">message_id</span><span class="o">=</span><span class="n">message_id</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">pin</span><span class="p">)</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">pin</span><span class="p">)</span>
<span class="k">return</span> <span class="n">pin</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.set_user_phone" class="doc doc-heading">
<code class="highlight language-python"><span class="n">set_user_phone</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">phone</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Set or clear phone for user by Telegram user id.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>phone</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Phone string or None to clear.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Updated User or None if not found.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">453</span>
<span class="normal">454</span>
<span class="normal">455</span>
<span class="normal">456</span>
<span class="normal">457</span>
<span class="normal">458</span>
<span class="normal">459</span>
<span class="normal">460</span>
<span class="normal">461</span>
<span class="normal">462</span>
<span class="normal">463</span>
<span class="normal">464</span>
<span class="normal">465</span>
<span class="normal">466</span>
<span class="normal">467</span>
<span class="normal">468</span>
<span class="normal">469</span>
<span class="normal">470</span>
<span class="normal">471</span>
<span class="normal">472</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">set_user_phone</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">phone</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Set or clear phone for user by Telegram user id.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> phone: Phone string or None to clear.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Updated User or None if not found.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">telegram_user_id</span> <span class="o">==</span> <span class="n">telegram_user_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="n">user</span><span class="o">.</span><span class="n">phone</span> <span class="o">=</span> <span class="n">phone</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.db.repository.update_user_display_name" class="doc doc-heading">
<code class="highlight language-python"><span class="n">update_user_display_name</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">full_name</span><span class="p">,</span> <span class="n">first_name</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">last_name</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Update display name and set name_manually_edited=True.</p>
<p>Use from API or admin when name is changed manually; subsequent
get_or_create_user will not overwrite these fields.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>telegram_user_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram user id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>full_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>New full name.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>first_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>New first name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>last_name</code>
</td>
<td>
<code><span title="str">str</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>New last name (optional).</p>
</div>
</td>
<td>
<code>None</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="User (duty_teller.db.models.User)" href="#duty_teller.db.models.User">User</a> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Updated User or None if not found.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/db/repository.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span>
<span class="normal">129</span>
<span class="normal">130</span>
<span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">update_user_display_name</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<span class="n">first_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">last_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="n">User</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Update display name and set name_manually_edited=True.</span>
<span class="sd"> Use from API or admin when name is changed manually; subsequent</span>
<span class="sd"> get_or_create_user will not overwrite these fields.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> telegram_user_id: Telegram user id.</span>
<span class="sd"> full_name: New full name.</span>
<span class="sd"> first_name: New first name (optional).</span>
<span class="sd"> last_name: New last name (optional).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Updated User or None if not found.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="n">User</span><span class="p">)</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">User</span><span class="o">.</span><span class="n">telegram_user_id</span> <span class="o">==</span> <span class="n">telegram_user_id</span><span class="p">)</span><span class="o">.</span><span class="n">first</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="n">user</span><span class="o">.</span><span class="n">full_name</span> <span class="o">=</span> <span class="n">full_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">first_name</span> <span class="o">=</span> <span class="n">first_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">last_name</span> <span class="o">=</span> <span class="n">last_name</span>
<span class="n">user</span><span class="o">.</span><span class="n">name_manually_edited</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">session</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
<span class="n">session</span><span class="o">.</span><span class="n">refresh</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="k">return</span> <span class="n">user</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div><h2 id="services">Services</h2>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.services" class="doc doc-heading">
<code>duty_teller.services</code>
</h2>
<div class="doc doc-contents first">
<p>Service layer: business logic and orchestration.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.delete_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">delete_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Remove the pinned message record for the chat (e.g. when bot leaves).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">delete_pin</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Remove the pinned message record for the chat (e.g. when bot leaves).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">delete_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.format_duty_message" class="doc doc-heading">
<code class="highlight language-python"><span class="n">format_duty_message</span><span class="p">(</span><span class="n">duty</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">,</span> <span class="n">lang</span><span class="o">=</span><span class="s1">&#39;en&#39;</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Build the text for the pinned duty message.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>duty</code>
</td>
<td>
</td>
<td>
<div class="doc-md-description">
<p>Duty instance or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user</code>
</td>
<td>
</td>
<td>
<div class="doc-md-description">
<p>User instance or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>tz_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Timezone name for display (e.g. Europe/Moscow).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>lang</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Language code for i18n ('ru' or 'en').</p>
</div>
</td>
<td>
<code>&#39;en&#39;</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Formatted message string; "No duty" if duty or user is None.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">format_duty_message</span><span class="p">(</span><span class="n">duty</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">lang</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Build the text for the pinned duty message.</span>
<span class="sd"> Args:</span>
<span class="sd"> duty: Duty instance or None.</span>
<span class="sd"> user: User instance or None.</span>
<span class="sd"> tz_name: Timezone name for display (e.g. Europe/Moscow).</span>
<span class="sd"> lang: Language code for i18n (&#39;ru&#39; or &#39;en&#39;).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Formatted message string; &quot;No duty&quot; if duty or user is None.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">duty</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="n">user</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;duty.no_duty&quot;</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">tz</span> <span class="o">=</span> <span class="n">ZoneInfo</span><span class="p">(</span><span class="n">tz_name</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="n">tz</span> <span class="o">=</span> <span class="n">ZoneInfo</span><span class="p">(</span><span class="s2">&quot;Europe/Moscow&quot;</span><span class="p">)</span>
<span class="n">tz_name</span> <span class="o">=</span> <span class="s2">&quot;Europe/Moscow&quot;</span>
<span class="n">start_dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">duty</span><span class="o">.</span><span class="n">start_at</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;Z&quot;</span><span class="p">,</span> <span class="s2">&quot;+00:00&quot;</span><span class="p">))</span>
<span class="n">end_dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">duty</span><span class="o">.</span><span class="n">end_at</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;Z&quot;</span><span class="p">,</span> <span class="s2">&quot;+00:00&quot;</span><span class="p">))</span>
<span class="n">start_local</span> <span class="o">=</span> <span class="n">start_dt</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="p">)</span>
<span class="n">end_local</span> <span class="o">=</span> <span class="n">end_dt</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="p">)</span>
<span class="n">offset_sec</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">start_local</span><span class="o">.</span><span class="n">utcoffset</span><span class="p">()</span><span class="o">.</span><span class="n">total_seconds</span><span class="p">()</span> <span class="k">if</span> <span class="n">start_local</span><span class="o">.</span><span class="n">utcoffset</span><span class="p">()</span> <span class="k">else</span> <span class="mi">0</span>
<span class="p">)</span>
<span class="n">sign</span> <span class="o">=</span> <span class="s2">&quot;+&quot;</span> <span class="k">if</span> <span class="n">offset_sec</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;-&quot;</span>
<span class="n">h</span><span class="p">,</span> <span class="n">r</span> <span class="o">=</span> <span class="nb">divmod</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">offset_sec</span><span class="p">)),</span> <span class="mi">3600</span><span class="p">)</span>
<span class="n">m</span> <span class="o">=</span> <span class="n">r</span> <span class="o">//</span> <span class="mi">60</span>
<span class="n">tz_hint</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;UTC</span><span class="si">{</span><span class="n">sign</span><span class="si">}{</span><span class="n">h</span><span class="si">:</span><span class="s2">d</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">m</span><span class="si">:</span><span class="s2">02d</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">tz_name</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">time_range</span> <span class="o">=</span> <span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">start_local</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">%d</span><span class="s1">.%m.%Y %H:%M&#39;</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">end_local</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">%d</span><span class="s1">.%m.%Y %H:%M&#39;</span><span class="p">)</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">tz_hint</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="p">)</span>
<span class="n">label</span> <span class="o">=</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;duty.label&quot;</span><span class="p">)</span>
<span class="n">lines</span> <span class="o">=</span> <span class="p">[</span>
<span class="sa">f</span><span class="s2">&quot;🕐 </span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">time_range</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span>
<span class="sa">f</span><span class="s2">&quot;👤 </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">full_name</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span>
<span class="p">]</span>
<span class="k">if</span> <span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="p">:</span>
<span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;📞 </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="p">:</span>
<span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;@</span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">lines</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.get_all_pin_chat_ids" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_all_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return all chat_ids that have a pinned duty message.</p>
<p>Used to restore update jobs on bot startup.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="int">int</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of chat ids.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span>
<span class="normal">136</span>
<span class="normal">137</span>
<span class="normal">138</span>
<span class="normal">139</span>
<span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_all_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return all chat_ids that have a pinned duty message.</span>
<span class="sd"> Used to restore update jobs on bot startup.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of chat ids.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_all_group_duty_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.get_duty_message_text" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_duty_message_text</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">,</span> <span class="n">lang</span><span class="o">=</span><span class="s1">&#39;en&#39;</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Get current duty from DB and return formatted message text.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>tz_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Timezone name for display.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>lang</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Language code for i18n.</p>
</div>
</td>
<td>
<code>&#39;en&#39;</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Formatted duty message or "No duty" if none.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span>
<span class="normal">73</span>
<span class="normal">74</span>
<span class="normal">75</span>
<span class="normal">76</span>
<span class="normal">77</span>
<span class="normal">78</span>
<span class="normal">79</span>
<span class="normal">80</span>
<span class="normal">81</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_duty_message_text</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">lang</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Get current duty from DB and return formatted message text.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> tz_name: Timezone name for display.</span>
<span class="sd"> lang: Language code for i18n.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Formatted duty message or &quot;No duty&quot; if none.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">get_current_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">now</span><span class="p">)</span>
<span class="k">if</span> <span class="n">result</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;duty.no_duty&quot;</span><span class="p">)</span>
<span class="n">duty</span><span class="p">,</span> <span class="n">user</span> <span class="o">=</span> <span class="n">result</span>
<span class="k">return</span> <span class="n">format_duty_message</span><span class="p">(</span><span class="n">duty</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.get_message_id" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_message_id</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return message_id for the pinned duty message in this chat.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="int">int</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Message id or None if no pin record.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_message_id</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return message_id for the pinned duty message in this chat.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Message id or None if no pin record.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">pin</span> <span class="o">=</span> <span class="n">get_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
<span class="k">return</span> <span class="n">pin</span><span class="o">.</span><span class="n">message_id</span> <span class="k">if</span> <span class="n">pin</span> <span class="k">else</span> <span class="kc">None</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.get_next_shift_end_utc" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_next_shift_end_utc</span><span class="p">(</span><span class="n">session</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return next shift end as naive UTC datetime for job scheduling.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="datetime.datetime">datetime</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Next shift end (naive UTC) or None.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">84</span>
<span class="normal">85</span>
<span class="normal">86</span>
<span class="normal">87</span>
<span class="normal">88</span>
<span class="normal">89</span>
<span class="normal">90</span>
<span class="normal">91</span>
<span class="normal">92</span>
<span class="normal">93</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_next_shift_end_utc</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">datetime</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return next shift end as naive UTC datetime for job scheduling.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Next shift end (naive UTC) or None.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_next_shift_end</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.run_import" class="doc doc-heading">
<code class="highlight language-python"><span class="n">run_import</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Run duty-schedule import: delete range per user, insert duty/unavailable/vacation.</p>
<p>For each entry: get_or_create_user_by_full_name, delete_duties_in_range for
the result date range, then insert duties (handover time in UTC), unavailable
(all-day), and vacation (consecutive ranges).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>result</code>
</td>
<td>
<code><a class="autorefs autorefs-internal" title="DutyScheduleResult
dataclass
(duty_teller.importers.duty_schedule.DutyScheduleResult)" href="#duty_teller.importers.duty_schedule.DutyScheduleResult">DutyScheduleResult</a></code>
</td>
<td>
<div class="doc-md-description">
<p>Parsed duty schedule (start_date, end_date, entries).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>hour_utc</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Handover hour in UTC (0-23).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>minute_utc</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Handover minute in UTC (0-59).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="tuple">tuple</span>[<span title="int">int</span>, <span title="int">int</span>, <span title="int">int</span>, <span title="int">int</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>Tuple (num_users, num_duty, num_unavailable, num_vacation).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/import_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span>
<span class="normal">64</span>
<span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span>
<span class="normal">73</span>
<span class="normal">74</span>
<span class="normal">75</span>
<span class="normal">76</span>
<span class="normal">77</span>
<span class="normal">78</span>
<span class="normal">79</span>
<span class="normal">80</span>
<span class="normal">81</span>
<span class="normal">82</span>
<span class="normal">83</span>
<span class="normal">84</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">run_import</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">result</span><span class="p">:</span> <span class="n">DutyScheduleResult</span><span class="p">,</span>
<span class="n">hour_utc</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">minute_utc</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Run duty-schedule import: delete range per user, insert duty/unavailable/vacation.</span>
<span class="sd"> For each entry: get_or_create_user_by_full_name, delete_duties_in_range for</span>
<span class="sd"> the result date range, then insert duties (handover time in UTC), unavailable</span>
<span class="sd"> (all-day), and vacation (consecutive ranges).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> result: Parsed duty schedule (start_date, end_date, entries).</span>
<span class="sd"> hour_utc: Handover hour in UTC (0-23).</span>
<span class="sd"> minute_utc: Handover minute in UTC (0-59).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Tuple (num_users, num_duty, num_unavailable, num_vacation).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">from_date_str</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">start_date</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">to_date_str</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">end_date</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">num_duty</span> <span class="o">=</span> <span class="n">num_unavailable</span> <span class="o">=</span> <span class="n">num_vacation</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">result</span><span class="o">.</span><span class="n">entries</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_or_create_user_by_full_name</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">entry</span><span class="o">.</span><span class="n">full_name</span><span class="p">)</span>
<span class="n">delete_duties_in_range</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">from_date_str</span><span class="p">,</span> <span class="n">to_date_str</span><span class="p">)</span>
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">duty_dates</span><span class="p">:</span>
<span class="n">start_at</span> <span class="o">=</span> <span class="n">duty_to_iso</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span>
<span class="n">d_next</span> <span class="o">=</span> <span class="n">d</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">end_at</span> <span class="o">=</span> <span class="n">duty_to_iso</span><span class="p">(</span><span class="n">d_next</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span>
<span class="n">insert_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">start_at</span><span class="p">,</span> <span class="n">end_at</span><span class="p">,</span> <span class="n">event_type</span><span class="o">=</span><span class="s2">&quot;duty&quot;</span><span class="p">)</span>
<span class="n">num_duty</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">unavailable_dates</span><span class="p">:</span>
<span class="n">insert_duty</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">day_start_iso</span><span class="p">(</span><span class="n">d</span><span class="p">),</span>
<span class="n">day_end_iso</span><span class="p">(</span><span class="n">d</span><span class="p">),</span>
<span class="n">event_type</span><span class="o">=</span><span class="s2">&quot;unavailable&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">num_unavailable</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">for</span> <span class="n">start_d</span><span class="p">,</span> <span class="n">end_d</span> <span class="ow">in</span> <span class="n">_consecutive_date_ranges</span><span class="p">(</span><span class="n">entry</span><span class="o">.</span><span class="n">vacation_dates</span><span class="p">):</span>
<span class="n">insert_duty</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">day_start_iso</span><span class="p">(</span><span class="n">start_d</span><span class="p">),</span>
<span class="n">day_end_iso</span><span class="p">(</span><span class="n">end_d</span><span class="p">),</span>
<span class="n">event_type</span><span class="o">=</span><span class="s2">&quot;vacation&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">num_vacation</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">entries</span><span class="p">),</span> <span class="n">num_duty</span><span class="p">,</span> <span class="n">num_unavailable</span><span class="p">,</span> <span class="n">num_vacation</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.save_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">save_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">message_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Save or update the pinned duty message record for a chat.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>message_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Message id to store.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">save_pin</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">message_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Save or update the pinned duty message record for a chat.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> message_id: Message id to store.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">save_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">message_id</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.services.import_service" class="doc doc-heading">
<code>duty_teller.services.import_service</code>
</h2>
<div class="doc doc-contents first">
<p>Import duty schedule: delete range, insert duties/unavailable/vacation. Accepts session.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.import_service.run_import" class="doc doc-heading">
<code class="highlight language-python"><span class="n">run_import</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Run duty-schedule import: delete range per user, insert duty/unavailable/vacation.</p>
<p>For each entry: get_or_create_user_by_full_name, delete_duties_in_range for
the result date range, then insert duties (handover time in UTC), unavailable
(all-day), and vacation (consecutive ranges).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>result</code>
</td>
<td>
<code><a class="autorefs autorefs-internal" title="DutyScheduleResult
dataclass
(duty_teller.importers.duty_schedule.DutyScheduleResult)" href="#duty_teller.importers.duty_schedule.DutyScheduleResult">DutyScheduleResult</a></code>
</td>
<td>
<div class="doc-md-description">
<p>Parsed duty schedule (start_date, end_date, entries).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>hour_utc</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Handover hour in UTC (0-23).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>minute_utc</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Handover minute in UTC (0-59).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="tuple">tuple</span>[<span title="int">int</span>, <span title="int">int</span>, <span title="int">int</span>, <span title="int">int</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>Tuple (num_users, num_duty, num_unavailable, num_vacation).</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/import_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span>
<span class="normal">64</span>
<span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span>
<span class="normal">73</span>
<span class="normal">74</span>
<span class="normal">75</span>
<span class="normal">76</span>
<span class="normal">77</span>
<span class="normal">78</span>
<span class="normal">79</span>
<span class="normal">80</span>
<span class="normal">81</span>
<span class="normal">82</span>
<span class="normal">83</span>
<span class="normal">84</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">run_import</span><span class="p">(</span>
<span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span>
<span class="n">result</span><span class="p">:</span> <span class="n">DutyScheduleResult</span><span class="p">,</span>
<span class="n">hour_utc</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="n">minute_utc</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">,</span> <span class="nb">int</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Run duty-schedule import: delete range per user, insert duty/unavailable/vacation.</span>
<span class="sd"> For each entry: get_or_create_user_by_full_name, delete_duties_in_range for</span>
<span class="sd"> the result date range, then insert duties (handover time in UTC), unavailable</span>
<span class="sd"> (all-day), and vacation (consecutive ranges).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> result: Parsed duty schedule (start_date, end_date, entries).</span>
<span class="sd"> hour_utc: Handover hour in UTC (0-23).</span>
<span class="sd"> minute_utc: Handover minute in UTC (0-59).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Tuple (num_users, num_duty, num_unavailable, num_vacation).</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">from_date_str</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">start_date</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">to_date_str</span> <span class="o">=</span> <span class="n">result</span><span class="o">.</span><span class="n">end_date</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
<span class="n">num_duty</span> <span class="o">=</span> <span class="n">num_unavailable</span> <span class="o">=</span> <span class="n">num_vacation</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">result</span><span class="o">.</span><span class="n">entries</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_or_create_user_by_full_name</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">entry</span><span class="o">.</span><span class="n">full_name</span><span class="p">)</span>
<span class="n">delete_duties_in_range</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">from_date_str</span><span class="p">,</span> <span class="n">to_date_str</span><span class="p">)</span>
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">duty_dates</span><span class="p">:</span>
<span class="n">start_at</span> <span class="o">=</span> <span class="n">duty_to_iso</span><span class="p">(</span><span class="n">d</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span>
<span class="n">d_next</span> <span class="o">=</span> <span class="n">d</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
<span class="n">end_at</span> <span class="o">=</span> <span class="n">duty_to_iso</span><span class="p">(</span><span class="n">d_next</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span>
<span class="n">insert_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span> <span class="n">start_at</span><span class="p">,</span> <span class="n">end_at</span><span class="p">,</span> <span class="n">event_type</span><span class="o">=</span><span class="s2">&quot;duty&quot;</span><span class="p">)</span>
<span class="n">num_duty</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="n">entry</span><span class="o">.</span><span class="n">unavailable_dates</span><span class="p">:</span>
<span class="n">insert_duty</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">day_start_iso</span><span class="p">(</span><span class="n">d</span><span class="p">),</span>
<span class="n">day_end_iso</span><span class="p">(</span><span class="n">d</span><span class="p">),</span>
<span class="n">event_type</span><span class="o">=</span><span class="s2">&quot;unavailable&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">num_unavailable</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">for</span> <span class="n">start_d</span><span class="p">,</span> <span class="n">end_d</span> <span class="ow">in</span> <span class="n">_consecutive_date_ranges</span><span class="p">(</span><span class="n">entry</span><span class="o">.</span><span class="n">vacation_dates</span><span class="p">):</span>
<span class="n">insert_duty</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">,</span>
<span class="n">day_start_iso</span><span class="p">(</span><span class="n">start_d</span><span class="p">),</span>
<span class="n">day_end_iso</span><span class="p">(</span><span class="n">end_d</span><span class="p">),</span>
<span class="n">event_type</span><span class="o">=</span><span class="s2">&quot;vacation&quot;</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">num_vacation</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="k">return</span> <span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">entries</span><span class="p">),</span> <span class="n">num_duty</span><span class="p">,</span> <span class="n">num_unavailable</span><span class="p">,</span> <span class="n">num_vacation</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.services.group_duty_pin_service" class="doc doc-heading">
<code>duty_teller.services.group_duty_pin_service</code>
</h2>
<div class="doc doc-contents first">
<p>Group duty pin: current duty message text, next shift end, pin CRUD. All accept session.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.delete_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">delete_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Remove the pinned message record for the chat (e.g. when bot leaves).</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">delete_pin</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Remove the pinned message record for the chat (e.g. when bot leaves).</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">delete_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.format_duty_message" class="doc doc-heading">
<code class="highlight language-python"><span class="n">format_duty_message</span><span class="p">(</span><span class="n">duty</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">,</span> <span class="n">lang</span><span class="o">=</span><span class="s1">&#39;en&#39;</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Build the text for the pinned duty message.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>duty</code>
</td>
<td>
</td>
<td>
<div class="doc-md-description">
<p>Duty instance or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>user</code>
</td>
<td>
</td>
<td>
<div class="doc-md-description">
<p>User instance or None.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>tz_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Timezone name for display (e.g. Europe/Moscow).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>lang</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Language code for i18n ('ru' or 'en').</p>
</div>
</td>
<td>
<code>&#39;en&#39;</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Formatted message string; "No duty" if duty or user is None.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">format_duty_message</span><span class="p">(</span><span class="n">duty</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">lang</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Build the text for the pinned duty message.</span>
<span class="sd"> Args:</span>
<span class="sd"> duty: Duty instance or None.</span>
<span class="sd"> user: User instance or None.</span>
<span class="sd"> tz_name: Timezone name for display (e.g. Europe/Moscow).</span>
<span class="sd"> lang: Language code for i18n (&#39;ru&#39; or &#39;en&#39;).</span>
<span class="sd"> Returns:</span>
<span class="sd"> Formatted message string; &quot;No duty&quot; if duty or user is None.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">duty</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="n">user</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;duty.no_duty&quot;</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">tz</span> <span class="o">=</span> <span class="n">ZoneInfo</span><span class="p">(</span><span class="n">tz_name</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="n">tz</span> <span class="o">=</span> <span class="n">ZoneInfo</span><span class="p">(</span><span class="s2">&quot;Europe/Moscow&quot;</span><span class="p">)</span>
<span class="n">tz_name</span> <span class="o">=</span> <span class="s2">&quot;Europe/Moscow&quot;</span>
<span class="n">start_dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">duty</span><span class="o">.</span><span class="n">start_at</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;Z&quot;</span><span class="p">,</span> <span class="s2">&quot;+00:00&quot;</span><span class="p">))</span>
<span class="n">end_dt</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">duty</span><span class="o">.</span><span class="n">end_at</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;Z&quot;</span><span class="p">,</span> <span class="s2">&quot;+00:00&quot;</span><span class="p">))</span>
<span class="n">start_local</span> <span class="o">=</span> <span class="n">start_dt</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="p">)</span>
<span class="n">end_local</span> <span class="o">=</span> <span class="n">end_dt</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="p">)</span>
<span class="n">offset_sec</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">start_local</span><span class="o">.</span><span class="n">utcoffset</span><span class="p">()</span><span class="o">.</span><span class="n">total_seconds</span><span class="p">()</span> <span class="k">if</span> <span class="n">start_local</span><span class="o">.</span><span class="n">utcoffset</span><span class="p">()</span> <span class="k">else</span> <span class="mi">0</span>
<span class="p">)</span>
<span class="n">sign</span> <span class="o">=</span> <span class="s2">&quot;+&quot;</span> <span class="k">if</span> <span class="n">offset_sec</span> <span class="o">&gt;=</span> <span class="mi">0</span> <span class="k">else</span> <span class="s2">&quot;-&quot;</span>
<span class="n">h</span><span class="p">,</span> <span class="n">r</span> <span class="o">=</span> <span class="nb">divmod</span><span class="p">(</span><span class="nb">abs</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">offset_sec</span><span class="p">)),</span> <span class="mi">3600</span><span class="p">)</span>
<span class="n">m</span> <span class="o">=</span> <span class="n">r</span> <span class="o">//</span> <span class="mi">60</span>
<span class="n">tz_hint</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;UTC</span><span class="si">{</span><span class="n">sign</span><span class="si">}{</span><span class="n">h</span><span class="si">:</span><span class="s2">d</span><span class="si">}</span><span class="s2">:</span><span class="si">{</span><span class="n">m</span><span class="si">:</span><span class="s2">02d</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">tz_name</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">time_range</span> <span class="o">=</span> <span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">start_local</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">%d</span><span class="s1">.%m.%Y %H:%M&#39;</span><span class="p">)</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">end_local</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s1">&#39;</span><span class="si">%d</span><span class="s1">.%m.%Y %H:%M&#39;</span><span class="p">)</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">tz_hint</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="p">)</span>
<span class="n">label</span> <span class="o">=</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;duty.label&quot;</span><span class="p">)</span>
<span class="n">lines</span> <span class="o">=</span> <span class="p">[</span>
<span class="sa">f</span><span class="s2">&quot;🕐 </span><span class="si">{</span><span class="n">label</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">time_range</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span>
<span class="sa">f</span><span class="s2">&quot;👤 </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">full_name</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">,</span>
<span class="p">]</span>
<span class="k">if</span> <span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="p">:</span>
<span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;📞 </span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="p">:</span>
<span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;@</span><span class="si">{</span><span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">lines</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.get_all_pin_chat_ids" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_all_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return all chat_ids that have a pinned duty message.</p>
<p>Used to restore update jobs on bot startup.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="list">list</span>[<span title="int">int</span>]</code>
</td>
<td>
<div class="doc-md-description">
<p>List of chat ids.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span>
<span class="normal">136</span>
<span class="normal">137</span>
<span class="normal">138</span>
<span class="normal">139</span>
<span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_all_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return all chat_ids that have a pinned duty message.</span>
<span class="sd"> Used to restore update jobs on bot startup.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> Returns:</span>
<span class="sd"> List of chat ids.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_all_group_duty_pin_chat_ids</span><span class="p">(</span><span class="n">session</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.get_duty_message_text" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_duty_message_text</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">,</span> <span class="n">lang</span><span class="o">=</span><span class="s1">&#39;en&#39;</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Get current duty from DB and return formatted message text.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>tz_name</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Timezone name for display.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>lang</code>
</td>
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Language code for i18n.</p>
</div>
</td>
<td>
<code>&#39;en&#39;</code>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="str">str</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Formatted duty message or "No duty" if none.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span>
<span class="normal">73</span>
<span class="normal">74</span>
<span class="normal">75</span>
<span class="normal">76</span>
<span class="normal">77</span>
<span class="normal">78</span>
<span class="normal">79</span>
<span class="normal">80</span>
<span class="normal">81</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_duty_message_text</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">lang</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s2">&quot;en&quot;</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Get current duty from DB and return formatted message text.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> tz_name: Timezone name for display.</span>
<span class="sd"> lang: Language code for i18n.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Formatted duty message or &quot;No duty&quot; if none.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">get_current_duty</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">now</span><span class="p">)</span>
<span class="k">if</span> <span class="n">result</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;duty.no_duty&quot;</span><span class="p">)</span>
<span class="n">duty</span><span class="p">,</span> <span class="n">user</span> <span class="o">=</span> <span class="n">result</span>
<span class="k">return</span> <span class="n">format_duty_message</span><span class="p">(</span><span class="n">duty</span><span class="p">,</span> <span class="n">user</span><span class="p">,</span> <span class="n">tz_name</span><span class="p">,</span> <span class="n">lang</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.get_message_id" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_message_id</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return message_id for the pinned duty message in this chat.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="int">int</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Message id or None if no pin record.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_message_id</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return message_id for the pinned duty message in this chat.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Message id or None if no pin record.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">pin</span> <span class="o">=</span> <span class="n">get_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
<span class="k">return</span> <span class="n">pin</span><span class="o">.</span><span class="n">message_id</span> <span class="k">if</span> <span class="n">pin</span> <span class="k">else</span> <span class="kc">None</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.get_next_shift_end_utc" class="doc doc-heading">
<code class="highlight language-python"><span class="n">get_next_shift_end_utc</span><span class="p">(</span><span class="n">session</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Return next shift end as naive UTC datetime for job scheduling.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><span title="datetime.datetime">datetime</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Next shift end (naive UTC) or None.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">84</span>
<span class="normal">85</span>
<span class="normal">86</span>
<span class="normal">87</span>
<span class="normal">88</span>
<span class="normal">89</span>
<span class="normal">90</span>
<span class="normal">91</span>
<span class="normal">92</span>
<span class="normal">93</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">get_next_shift_end_utc</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">datetime</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Return next shift end as naive UTC datetime for job scheduling.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> Returns:</span>
<span class="sd"> Next shift end (naive UTC) or None.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">get_next_shift_end</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">(</span><span class="n">timezone</span><span class="o">.</span><span class="n">utc</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.services.group_duty_pin_service.save_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">save_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">message_id</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Save or update the pinned duty message record for a chat.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>session</code>
</td>
<td>
<code><span title="sqlalchemy.orm.Session">Session</span></code>
</td>
<td>
<div class="doc-md-description">
<p>DB session.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>chat_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Telegram chat id.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>message_id</code>
</td>
<td>
<code><span title="int">int</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Message id to store.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/services/group_duty_pin_service.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">save_pin</span><span class="p">(</span><span class="n">session</span><span class="p">:</span> <span class="n">Session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">message_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Save or update the pinned duty message record for a chat.</span>
<span class="sd"> Args:</span>
<span class="sd"> session: DB session.</span>
<span class="sd"> chat_id: Telegram chat id.</span>
<span class="sd"> message_id: Message id to store.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">save_group_duty_pin</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">message_id</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div><h2 id="handlers">Handlers</h2>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.handlers" class="doc doc-heading">
<code>duty_teller.handlers</code>
</h2>
<div class="doc doc-contents first">
<p>Expose a single register_handlers(app) that registers all handlers.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.register_handlers" class="doc doc-heading">
<code class="highlight language-python"><span class="n">register_handlers</span><span class="p">(</span><span class="n">app</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Register all Telegram handlers (commands, import, group pin, error handler) on the application.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>app</code>
</td>
<td>
<code><span title="telegram.ext.Application">Application</span></code>
</td>
<td>
<div class="doc-md-description">
<p>python-telegram-bot Application instance.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/__init__.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 8</span>
<span class="normal"> 9</span>
<span class="normal">10</span>
<span class="normal">11</span>
<span class="normal">12</span>
<span class="normal">13</span>
<span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">register_handlers</span><span class="p">(</span><span class="n">app</span><span class="p">:</span> <span class="n">Application</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Register all Telegram handlers (commands, import, group pin, error handler) on the application.</span>
<span class="sd"> Args:</span>
<span class="sd"> app: python-telegram-bot Application instance.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">commands</span><span class="o">.</span><span class="n">start_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">commands</span><span class="o">.</span><span class="n">help_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">commands</span><span class="o">.</span><span class="n">set_phone_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">commands</span><span class="o">.</span><span class="n">calendar_link_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">import_duty_schedule</span><span class="o">.</span><span class="n">import_duty_schedule_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">import_duty_schedule</span><span class="o">.</span><span class="n">handover_time_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">import_duty_schedule</span><span class="o">.</span><span class="n">duty_schedule_document_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">group_duty_pin</span><span class="o">.</span><span class="n">group_duty_pin_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_handler</span><span class="p">(</span><span class="n">group_duty_pin</span><span class="o">.</span><span class="n">pin_duty_handler</span><span class="p">)</span>
<span class="n">app</span><span class="o">.</span><span class="n">add_error_handler</span><span class="p">(</span><span class="n">errors</span><span class="o">.</span><span class="n">error_handler</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.handlers.commands" class="doc doc-heading">
<code>duty_teller.handlers.commands</code>
</h2>
<div class="doc doc-contents first">
<p>Command handlers: /start, /help; /start registers user.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.commands.calendar_link" class="doc doc-heading">
<code class="highlight language-python"><span class="n">calendar_link</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle /calendar_link: send personal ICS URL (private chat only; user must be in allowlist).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/commands.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span>
<span class="normal">129</span>
<span class="normal">130</span>
<span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span>
<span class="normal">136</span>
<span class="normal">137</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">calendar_link</span><span class="p">(</span><span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle /calendar_link: send personal ICS URL (private chat only; user must be in allowlist).&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="k">if</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span> <span class="ow">and</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span><span class="o">.</span><span class="n">type</span> <span class="o">!=</span> <span class="s2">&quot;private&quot;</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;calendar_link.private_only&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="n">telegram_user_id</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">id</span>
<span class="n">username</span> <span class="o">=</span> <span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">full_name</span> <span class="o">=</span> <span class="n">build_full_name</span><span class="p">(</span>
<span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">first_name</span><span class="p">,</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">last_name</span>
<span class="p">)</span>
<span class="k">def</span><span class="w"> </span><span class="nf">do_calendar_link</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">,</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">]:</span>
<span class="k">with</span> <span class="n">session_scope</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">DATABASE_URL</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">get_or_create_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="n">telegram_user_id</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">first_name</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">last_name</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">config</span><span class="o">.</span><span class="n">can_access_miniapp</span><span class="p">(</span>
<span class="n">username</span>
<span class="p">)</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">config</span><span class="o">.</span><span class="n">can_access_miniapp_by_phone</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">phone</span><span class="p">):</span>
<span class="k">return</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="s2">&quot;denied&quot;</span><span class="p">)</span>
<span class="n">token</span> <span class="o">=</span> <span class="n">create_calendar_token</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
<span class="n">base</span> <span class="o">=</span> <span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">MINI_APP_BASE_URL</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">rstrip</span><span class="p">(</span><span class="s2">&quot;/&quot;</span><span class="p">)</span>
<span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">base</span><span class="si">}</span><span class="s2">/api/calendar/ical/</span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="s2">.ics&quot;</span> <span class="k">if</span> <span class="n">base</span> <span class="k">else</span> <span class="kc">None</span>
<span class="k">return</span> <span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="n">result_url</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span>
<span class="kc">None</span><span class="p">,</span> <span class="n">do_calendar_link</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">&quot;denied&quot;</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;calendar_link.access_denied&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">result_url</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;calendar_link.error&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;calendar_link.success&quot;</span><span class="p">,</span> <span class="n">url</span><span class="o">=</span><span class="n">result_url</span><span class="p">)</span>
<span class="o">+</span> <span class="s2">&quot;</span><span class="se">\n\n</span><span class="s2">&quot;</span>
<span class="o">+</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;calendar_link.help_hint&quot;</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.commands.help_cmd" class="doc doc-heading">
<code class="highlight language-python"><span class="n">help_cmd</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle /help: send list of commands (admins see import_duty_schedule).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/commands.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span>
<span class="normal">143</span>
<span class="normal">144</span>
<span class="normal">145</span>
<span class="normal">146</span>
<span class="normal">147</span>
<span class="normal">148</span>
<span class="normal">149</span>
<span class="normal">150</span>
<span class="normal">151</span>
<span class="normal">152</span>
<span class="normal">153</span>
<span class="normal">154</span>
<span class="normal">155</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">help_cmd</span><span class="p">(</span><span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle /help: send list of commands (admins see import_duty_schedule).&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="n">lines</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.title&quot;</span><span class="p">),</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.start&quot;</span><span class="p">),</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.help&quot;</span><span class="p">),</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.set_phone&quot;</span><span class="p">),</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.calendar_link&quot;</span><span class="p">),</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.pin_duty&quot;</span><span class="p">),</span>
<span class="p">]</span>
<span class="k">if</span> <span class="n">config</span><span class="o">.</span><span class="n">is_admin</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">):</span>
<span class="n">lines</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;help.import_schedule&quot;</span><span class="p">))</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">lines</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.commands.set_phone" class="doc doc-heading">
<code class="highlight language-python"><span class="n">set_phone</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle /set_phone [number]: set or clear phone (private chat only).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/commands.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span>
<span class="normal">54</span>
<span class="normal">55</span>
<span class="normal">56</span>
<span class="normal">57</span>
<span class="normal">58</span>
<span class="normal">59</span>
<span class="normal">60</span>
<span class="normal">61</span>
<span class="normal">62</span>
<span class="normal">63</span>
<span class="normal">64</span>
<span class="normal">65</span>
<span class="normal">66</span>
<span class="normal">67</span>
<span class="normal">68</span>
<span class="normal">69</span>
<span class="normal">70</span>
<span class="normal">71</span>
<span class="normal">72</span>
<span class="normal">73</span>
<span class="normal">74</span>
<span class="normal">75</span>
<span class="normal">76</span>
<span class="normal">77</span>
<span class="normal">78</span>
<span class="normal">79</span>
<span class="normal">80</span>
<span class="normal">81</span>
<span class="normal">82</span>
<span class="normal">83</span>
<span class="normal">84</span>
<span class="normal">85</span>
<span class="normal">86</span>
<span class="normal">87</span>
<span class="normal">88</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">set_phone</span><span class="p">(</span><span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle /set_phone [number]: set or clear phone (private chat only).&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="k">if</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span> <span class="ow">and</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span><span class="o">.</span><span class="n">type</span> <span class="o">!=</span> <span class="s2">&quot;private&quot;</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;set_phone.private_only&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="n">args</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">args</span> <span class="ow">or</span> <span class="p">[]</span>
<span class="n">phone</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">args</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">if</span> <span class="n">args</span> <span class="k">else</span> <span class="kc">None</span>
<span class="n">telegram_user_id</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">id</span>
<span class="k">def</span><span class="w"> </span><span class="nf">do_set_phone</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">with</span> <span class="n">session_scope</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">DATABASE_URL</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
<span class="n">full_name</span> <span class="o">=</span> <span class="n">build_full_name</span><span class="p">(</span>
<span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">first_name</span><span class="p">,</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">last_name</span>
<span class="p">)</span>
<span class="n">get_or_create_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="n">telegram_user_id</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">first_name</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">last_name</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">set_user_phone</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">telegram_user_id</span><span class="p">,</span> <span class="n">phone</span> <span class="ow">or</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">if</span> <span class="n">user</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">&quot;error&quot;</span>
<span class="k">if</span> <span class="n">phone</span><span class="p">:</span>
<span class="k">return</span> <span class="s2">&quot;saved&quot;</span>
<span class="k">return</span> <span class="s2">&quot;cleared&quot;</span>
<span class="n">result</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">do_set_phone</span><span class="p">)</span>
<span class="k">if</span> <span class="n">result</span> <span class="o">==</span> <span class="s2">&quot;error&quot;</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;set_phone.error&quot;</span><span class="p">))</span>
<span class="k">elif</span> <span class="n">result</span> <span class="o">==</span> <span class="s2">&quot;saved&quot;</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;set_phone.saved&quot;</span><span class="p">,</span> <span class="n">phone</span><span class="o">=</span><span class="n">phone</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">))</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;set_phone.cleared&quot;</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.commands.start" class="doc doc-heading">
<code class="highlight language-python"><span class="n">start</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle /start: register user in DB and send greeting.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/commands.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span>
<span class="normal">31</span>
<span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">start</span><span class="p">(</span><span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle /start: register user in DB and send greeting.&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">user</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">full_name</span> <span class="o">=</span> <span class="n">build_full_name</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">first_name</span><span class="p">,</span> <span class="n">user</span><span class="o">.</span><span class="n">last_name</span><span class="p">)</span>
<span class="n">telegram_user_id</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">id</span>
<span class="n">username</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span>
<span class="n">first_name</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">first_name</span>
<span class="n">last_name</span> <span class="o">=</span> <span class="n">user</span><span class="o">.</span><span class="n">last_name</span>
<span class="k">def</span><span class="w"> </span><span class="nf">do_get_or_create</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">with</span> <span class="n">session_scope</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">DATABASE_URL</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
<span class="n">get_or_create_user</span><span class="p">(</span>
<span class="n">session</span><span class="p">,</span>
<span class="n">telegram_user_id</span><span class="o">=</span><span class="n">telegram_user_id</span><span class="p">,</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">username</span><span class="o">=</span><span class="n">username</span><span class="p">,</span>
<span class="n">first_name</span><span class="o">=</span><span class="n">first_name</span><span class="p">,</span>
<span class="n">last_name</span><span class="o">=</span><span class="n">last_name</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">do_get_or_create</span><span class="p">)</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;start.greeting&quot;</span><span class="p">)</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.handlers.import_duty_schedule" class="doc doc-heading">
<code>duty_teller.handlers.import_duty_schedule</code>
</h2>
<div class="doc doc-contents first">
<p>Import duty-schedule: /import_duty_schedule (admin only). Two steps: handover time -&gt; JSON file.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.import_duty_schedule.handle_duty_schedule_document" class="doc doc-heading">
<code class="highlight language-python"><span class="n">handle_duty_schedule_document</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle uploaded JSON file: parse duty-schedule and run import.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/import_duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 56</span>
<span class="normal"> 57</span>
<span class="normal"> 58</span>
<span class="normal"> 59</span>
<span class="normal"> 60</span>
<span class="normal"> 61</span>
<span class="normal"> 62</span>
<span class="normal"> 63</span>
<span class="normal"> 64</span>
<span class="normal"> 65</span>
<span class="normal"> 66</span>
<span class="normal"> 67</span>
<span class="normal"> 68</span>
<span class="normal"> 69</span>
<span class="normal"> 70</span>
<span class="normal"> 71</span>
<span class="normal"> 72</span>
<span class="normal"> 73</span>
<span class="normal"> 74</span>
<span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">handle_duty_schedule_document</span><span class="p">(</span>
<span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle uploaded JSON file: parse duty-schedule and run import.&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">document</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;awaiting_duty_schedule_file&quot;</span><span class="p">):</span>
<span class="k">return</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="n">handover</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;handover_utc_time&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">handover</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">config</span><span class="o">.</span><span class="n">is_admin</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">):</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">document</span><span class="o">.</span><span class="n">file_name</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">)</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span><span class="o">.</span><span class="n">endswith</span><span class="p">(</span><span class="s2">&quot;.json&quot;</span><span class="p">):</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.need_json&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span> <span class="o">=</span> <span class="n">handover</span>
<span class="n">file_id</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">document</span><span class="o">.</span><span class="n">file_id</span>
<span class="n">file</span> <span class="o">=</span> <span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">get_file</span><span class="p">(</span><span class="n">file_id</span><span class="p">)</span>
<span class="n">raw</span> <span class="o">=</span> <span class="nb">bytes</span><span class="p">(</span><span class="k">await</span> <span class="n">file</span><span class="o">.</span><span class="n">download_as_bytearray</span><span class="p">())</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">parse_duty_schedule</span><span class="p">(</span><span class="n">raw</span><span class="p">)</span>
<span class="k">except</span> <span class="n">DutyScheduleParseError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">&quot;awaiting_duty_schedule_file&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">&quot;handover_utc_time&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.parse_error&quot;</span><span class="p">,</span> <span class="n">error</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)))</span>
<span class="k">return</span>
<span class="k">def</span><span class="w"> </span><span class="nf">run_import_with_scope</span><span class="p">():</span>
<span class="k">with</span> <span class="n">session_scope</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">DATABASE_URL</span><span class="p">)</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
<span class="k">return</span> <span class="n">run_import</span><span class="p">(</span><span class="n">session</span><span class="p">,</span> <span class="n">result</span><span class="p">,</span> <span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">num_users</span><span class="p">,</span> <span class="n">num_duty</span><span class="p">,</span> <span class="n">num_unavailable</span><span class="p">,</span> <span class="n">num_vacation</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span>
<span class="kc">None</span><span class="p">,</span> <span class="n">run_import_with_scope</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.import_error&quot;</span><span class="p">,</span> <span class="n">error</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">e</span><span class="p">)))</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">total</span> <span class="o">=</span> <span class="n">num_duty</span> <span class="o">+</span> <span class="n">num_unavailable</span> <span class="o">+</span> <span class="n">num_vacation</span>
<span class="n">unavailable_suffix</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.done_unavailable&quot;</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">num_unavailable</span><span class="p">))</span>
<span class="k">if</span> <span class="n">num_unavailable</span>
<span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="p">)</span>
<span class="n">vacation_suffix</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.done_vacation&quot;</span><span class="p">,</span> <span class="n">count</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">num_vacation</span><span class="p">))</span>
<span class="k">if</span> <span class="n">num_vacation</span>
<span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="p">)</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span>
<span class="n">t</span><span class="p">(</span>
<span class="n">lang</span><span class="p">,</span>
<span class="s2">&quot;import.done&quot;</span><span class="p">,</span>
<span class="n">users</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">num_users</span><span class="p">),</span>
<span class="n">duties</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">num_duty</span><span class="p">),</span>
<span class="n">unavailable</span><span class="o">=</span><span class="n">unavailable_suffix</span><span class="p">,</span>
<span class="n">vacation</span><span class="o">=</span><span class="n">vacation_suffix</span><span class="p">,</span>
<span class="n">total</span><span class="o">=</span><span class="nb">str</span><span class="p">(</span><span class="n">total</span><span class="p">),</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">finally</span><span class="p">:</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">&quot;awaiting_duty_schedule_file&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">pop</span><span class="p">(</span><span class="s2">&quot;handover_utc_time&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.import_duty_schedule.handle_handover_time_text" class="doc doc-heading">
<code class="highlight language-python"><span class="n">handle_handover_time_text</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle text message when awaiting handover time (e.g. 09:00 Europe/Moscow).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/import_duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span>
<span class="normal">36</span>
<span class="normal">37</span>
<span class="normal">38</span>
<span class="normal">39</span>
<span class="normal">40</span>
<span class="normal">41</span>
<span class="normal">42</span>
<span class="normal">43</span>
<span class="normal">44</span>
<span class="normal">45</span>
<span class="normal">46</span>
<span class="normal">47</span>
<span class="normal">48</span>
<span class="normal">49</span>
<span class="normal">50</span>
<span class="normal">51</span>
<span class="normal">52</span>
<span class="normal">53</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">handle_handover_time_text</span><span class="p">(</span>
<span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle text message when awaiting handover time (e.g. 09:00 Europe/Moscow).&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">text</span><span class="p">:</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;awaiting_handover_time&quot;</span><span class="p">):</span>
<span class="k">return</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">config</span><span class="o">.</span><span class="n">is_admin</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">):</span>
<span class="k">return</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">text</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">parsed</span> <span class="o">=</span> <span class="n">parse_handover_time</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
<span class="k">if</span> <span class="n">parsed</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.parse_time_error&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span> <span class="o">=</span> <span class="n">parsed</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="p">[</span><span class="s2">&quot;handover_utc_time&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="n">hour_utc</span><span class="p">,</span> <span class="n">minute_utc</span><span class="p">)</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="p">[</span><span class="s2">&quot;awaiting_handover_time&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">False</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="p">[</span><span class="s2">&quot;awaiting_duty_schedule_file&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.send_json&quot;</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.import_duty_schedule.import_duty_schedule_cmd" class="doc doc-heading">
<code class="highlight language-python"><span class="n">import_duty_schedule_cmd</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle /import_duty_schedule: start two-step import (admin only); asks for handover time.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/import_duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span>
<span class="normal">30</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">import_duty_schedule_cmd</span><span class="p">(</span>
<span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle /import_duty_schedule: start two-step import (admin only); asks for handover time.&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">config</span><span class="o">.</span><span class="n">is_admin</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="o">.</span><span class="n">username</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span><span class="p">):</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.admin_only&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="n">context</span><span class="o">.</span><span class="n">user_data</span><span class="p">[</span><span class="s2">&quot;awaiting_handover_time&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;import.handover_format&quot;</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.handlers.group_duty_pin" class="doc doc-heading">
<code>duty_teller.handlers.group_duty_pin</code>
</h2>
<div class="doc doc-contents first">
<p>Pinned duty message in groups: handle bot add/remove, schedule updates at shift end.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.group_duty_pin.my_chat_member_handler" class="doc doc-heading">
<code class="highlight language-python"><span class="n">my_chat_member_handler</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle bot added to or removed from group: send/pin duty message or delete pin record.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/group_duty_pin.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span>
<span class="normal">124</span>
<span class="normal">125</span>
<span class="normal">126</span>
<span class="normal">127</span>
<span class="normal">128</span>
<span class="normal">129</span>
<span class="normal">130</span>
<span class="normal">131</span>
<span class="normal">132</span>
<span class="normal">133</span>
<span class="normal">134</span>
<span class="normal">135</span>
<span class="normal">136</span>
<span class="normal">137</span>
<span class="normal">138</span>
<span class="normal">139</span>
<span class="normal">140</span>
<span class="normal">141</span>
<span class="normal">142</span>
<span class="normal">143</span>
<span class="normal">144</span>
<span class="normal">145</span>
<span class="normal">146</span>
<span class="normal">147</span>
<span class="normal">148</span>
<span class="normal">149</span>
<span class="normal">150</span>
<span class="normal">151</span>
<span class="normal">152</span>
<span class="normal">153</span>
<span class="normal">154</span>
<span class="normal">155</span>
<span class="normal">156</span>
<span class="normal">157</span>
<span class="normal">158</span>
<span class="normal">159</span>
<span class="normal">160</span>
<span class="normal">161</span>
<span class="normal">162</span>
<span class="normal">163</span>
<span class="normal">164</span>
<span class="normal">165</span>
<span class="normal">166</span>
<span class="normal">167</span>
<span class="normal">168</span>
<span class="normal">169</span>
<span class="normal">170</span>
<span class="normal">171</span>
<span class="normal">172</span>
<span class="normal">173</span>
<span class="normal">174</span>
<span class="normal">175</span>
<span class="normal">176</span>
<span class="normal">177</span>
<span class="normal">178</span>
<span class="normal">179</span>
<span class="normal">180</span>
<span class="normal">181</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">my_chat_member_handler</span><span class="p">(</span>
<span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle bot added to or removed from group: send/pin duty message or delete pin record.&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">my_chat_member</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">old</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">my_chat_member</span><span class="o">.</span><span class="n">old_chat_member</span>
<span class="n">new</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">my_chat_member</span><span class="o">.</span><span class="n">new_chat_member</span>
<span class="n">chat</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">chat</span> <span class="ow">or</span> <span class="n">chat</span><span class="o">.</span><span class="n">type</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s2">&quot;group&quot;</span><span class="p">,</span> <span class="s2">&quot;supergroup&quot;</span><span class="p">):</span>
<span class="k">return</span>
<span class="k">if</span> <span class="n">new</span><span class="o">.</span><span class="n">user</span><span class="o">.</span><span class="n">id</span> <span class="o">!=</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">id</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">chat_id</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">id</span>
<span class="k">if</span> <span class="n">new</span><span class="o">.</span><span class="n">status</span> <span class="ow">in</span> <span class="p">(</span>
<span class="n">ChatMemberStatus</span><span class="o">.</span><span class="n">MEMBER</span><span class="p">,</span>
<span class="n">ChatMemberStatus</span><span class="o">.</span><span class="n">ADMINISTRATOR</span><span class="p">,</span>
<span class="p">)</span> <span class="ow">and</span> <span class="n">old</span><span class="o">.</span><span class="n">status</span> <span class="ow">in</span> <span class="p">(</span>
<span class="n">ChatMemberStatus</span><span class="o">.</span><span class="n">LEFT</span><span class="p">,</span>
<span class="n">ChatMemberStatus</span><span class="o">.</span><span class="n">BANNED</span><span class="p">,</span>
<span class="p">):</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="n">text</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span>
<span class="kc">None</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="n">_get_duty_message_text_sync</span><span class="p">(</span><span class="n">lang</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">msg</span> <span class="o">=</span> <span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span><span class="n">chat_id</span><span class="o">=</span><span class="n">chat_id</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="n">text</span><span class="p">)</span>
<span class="k">except</span> <span class="p">(</span><span class="n">BadRequest</span><span class="p">,</span> <span class="n">Forbidden</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;Failed to send duty message in chat_id=</span><span class="si">%s</span><span class="s2">: </span><span class="si">%s</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
<span class="k">return</span>
<span class="n">pinned</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">pin_chat_message</span><span class="p">(</span>
<span class="n">chat_id</span><span class="o">=</span><span class="n">chat_id</span><span class="p">,</span>
<span class="n">message_id</span><span class="o">=</span><span class="n">msg</span><span class="o">.</span><span class="n">message_id</span><span class="p">,</span>
<span class="n">disable_notification</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">pinned</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">except</span> <span class="p">(</span><span class="n">BadRequest</span><span class="p">,</span> <span class="n">Forbidden</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;Failed to pin message in chat_id=</span><span class="si">%s</span><span class="s2">: </span><span class="si">%s</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
<span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_sync_save_pin</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">msg</span><span class="o">.</span><span class="n">message_id</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">pinned</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span>
<span class="n">chat_id</span><span class="o">=</span><span class="n">chat_id</span><span class="p">,</span>
<span class="n">text</span><span class="o">=</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;pin_duty.could_not_pin_make_admin&quot;</span><span class="p">),</span>
<span class="p">)</span>
<span class="k">except</span> <span class="p">(</span><span class="n">BadRequest</span><span class="p">,</span> <span class="n">Forbidden</span><span class="p">):</span>
<span class="k">pass</span>
<span class="n">next_end</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_get_next_shift_end_sync</span><span class="p">)</span>
<span class="k">await</span> <span class="n">_schedule_next_update</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">application</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">next_end</span><span class="p">)</span>
<span class="k">return</span>
<span class="k">if</span> <span class="n">new</span><span class="o">.</span><span class="n">status</span> <span class="ow">in</span> <span class="p">(</span><span class="n">ChatMemberStatus</span><span class="o">.</span><span class="n">LEFT</span><span class="p">,</span> <span class="n">ChatMemberStatus</span><span class="o">.</span><span class="n">BANNED</span><span class="p">):</span>
<span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span>
<span class="kc">None</span><span class="p">,</span> <span class="n">_sync_delete_pin</span><span class="p">,</span> <span class="n">chat_id</span>
<span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">JOB_NAME_PREFIX</span><span class="si">}{</span><span class="n">chat_id</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">if</span> <span class="n">context</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">job_queue</span><span class="p">:</span>
<span class="k">for</span> <span class="n">job</span> <span class="ow">in</span> <span class="n">context</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">job_queue</span><span class="o">.</span><span class="n">get_jobs_by_name</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
<span class="n">job</span><span class="o">.</span><span class="n">schedule_removal</span><span class="p">()</span>
<span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;Bot left chat_id=</span><span class="si">%s</span><span class="s2">, removed pin record and jobs&quot;</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.group_duty_pin.pin_duty_cmd" class="doc doc-heading">
<code class="highlight language-python"><span class="n">pin_duty_cmd</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Handle /pin_duty: pin the current duty message in the group (reply to bot's message).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/group_duty_pin.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">199</span>
<span class="normal">200</span>
<span class="normal">201</span>
<span class="normal">202</span>
<span class="normal">203</span>
<span class="normal">204</span>
<span class="normal">205</span>
<span class="normal">206</span>
<span class="normal">207</span>
<span class="normal">208</span>
<span class="normal">209</span>
<span class="normal">210</span>
<span class="normal">211</span>
<span class="normal">212</span>
<span class="normal">213</span>
<span class="normal">214</span>
<span class="normal">215</span>
<span class="normal">216</span>
<span class="normal">217</span>
<span class="normal">218</span>
<span class="normal">219</span>
<span class="normal">220</span>
<span class="normal">221</span>
<span class="normal">222</span>
<span class="normal">223</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">pin_duty_cmd</span><span class="p">(</span><span class="n">update</span><span class="p">:</span> <span class="n">Update</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Handle /pin_duty: pin the current duty message in the group (reply to bot&#39;s message).&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">chat</span> <span class="o">=</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_chat</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">update</span><span class="o">.</span><span class="n">effective_user</span><span class="p">)</span>
<span class="k">if</span> <span class="n">chat</span><span class="o">.</span><span class="n">type</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">(</span><span class="s2">&quot;group&quot;</span><span class="p">,</span> <span class="s2">&quot;supergroup&quot;</span><span class="p">):</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;pin_duty.group_only&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="n">chat_id</span> <span class="o">=</span> <span class="n">chat</span><span class="o">.</span><span class="n">id</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span>
<span class="n">message_id</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_sync_get_message_id</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">message_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;pin_duty.no_message&quot;</span><span class="p">))</span>
<span class="k">return</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">pin_chat_message</span><span class="p">(</span>
<span class="n">chat_id</span><span class="o">=</span><span class="n">chat_id</span><span class="p">,</span>
<span class="n">message_id</span><span class="o">=</span><span class="n">message_id</span><span class="p">,</span>
<span class="n">disable_notification</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;pin_duty.pinned&quot;</span><span class="p">))</span>
<span class="k">except</span> <span class="p">(</span><span class="n">BadRequest</span><span class="p">,</span> <span class="n">Forbidden</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;pin_duty failed chat_id=</span><span class="si">%s</span><span class="s2">: </span><span class="si">%s</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;pin_duty.failed&quot;</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.group_duty_pin.restore_group_pin_jobs" class="doc doc-heading">
<code class="highlight language-python"><span class="n">restore_group_pin_jobs</span><span class="p">(</span><span class="n">application</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Restore scheduled pin-update jobs for all chats that have a pinned message (on startup).</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/group_duty_pin.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">189</span>
<span class="normal">190</span>
<span class="normal">191</span>
<span class="normal">192</span>
<span class="normal">193</span>
<span class="normal">194</span>
<span class="normal">195</span>
<span class="normal">196</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">restore_group_pin_jobs</span><span class="p">(</span><span class="n">application</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Restore scheduled pin-update jobs for all chats that have a pinned message (on startup).&quot;&quot;&quot;</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span>
<span class="n">chat_ids</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_get_all_pin_chat_ids_sync</span><span class="p">)</span>
<span class="k">for</span> <span class="n">chat_id</span> <span class="ow">in</span> <span class="n">chat_ids</span><span class="p">:</span>
<span class="n">next_end</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_get_next_shift_end_sync</span><span class="p">)</span>
<span class="k">await</span> <span class="n">_schedule_next_update</span><span class="p">(</span><span class="n">application</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">next_end</span><span class="p">)</span>
<span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;Restored </span><span class="si">%s</span><span class="s2"> group pin jobs&quot;</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">chat_ids</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.group_duty_pin.update_group_pin" class="doc doc-heading">
<code class="highlight language-python"><span class="n">update_group_pin</span><span class="p">(</span><span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Job callback: refresh pinned duty message and schedule next update at shift end.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/group_duty_pin.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">update_group_pin</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Job callback: refresh pinned duty message and schedule next update at shift end.&quot;&quot;&quot;</span>
<span class="n">chat_id</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="n">job</span><span class="o">.</span><span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;chat_id&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">chat_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="k">return</span>
<span class="n">loop</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">get_running_loop</span><span class="p">()</span>
<span class="n">message_id</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_sync_get_message_id</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
<span class="k">if</span> <span class="n">message_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="s2">&quot;No pin record for chat_id=</span><span class="si">%s</span><span class="s2">, skipping update&quot;</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">)</span>
<span class="k">return</span>
<span class="n">text</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span>
<span class="kc">None</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="n">_get_duty_message_text_sync</span><span class="p">(</span><span class="n">config</span><span class="o">.</span><span class="n">DEFAULT_LANGUAGE</span><span class="p">)</span>
<span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">await</span> <span class="n">context</span><span class="o">.</span><span class="n">bot</span><span class="o">.</span><span class="n">edit_message_text</span><span class="p">(</span>
<span class="n">chat_id</span><span class="o">=</span><span class="n">chat_id</span><span class="p">,</span>
<span class="n">message_id</span><span class="o">=</span><span class="n">message_id</span><span class="p">,</span>
<span class="n">text</span><span class="o">=</span><span class="n">text</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">except</span> <span class="p">(</span><span class="n">BadRequest</span><span class="p">,</span> <span class="n">Forbidden</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;Failed to edit pinned message chat_id=</span><span class="si">%s</span><span class="s2">: </span><span class="si">%s</span><span class="s2">&quot;</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span>
<span class="n">next_end</span> <span class="o">=</span> <span class="k">await</span> <span class="n">loop</span><span class="o">.</span><span class="n">run_in_executor</span><span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="n">_get_next_shift_end_sync</span><span class="p">)</span>
<span class="k">await</span> <span class="n">_schedule_next_update</span><span class="p">(</span><span class="n">context</span><span class="o">.</span><span class="n">application</span><span class="p">,</span> <span class="n">chat_id</span><span class="p">,</span> <span class="n">next_end</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.handlers.errors" class="doc doc-heading">
<code>duty_teller.handlers.errors</code>
</h2>
<div class="doc doc-contents first">
<p>Global error handler: log exception and notify user.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-function">
<h3 id="duty_teller.handlers.errors.error_handler" class="doc doc-heading">
<code class="highlight language-python"><span class="n">error_handler</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">context</span><span class="p">)</span></code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-async"><code>async</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Global error handler: log exception and reply with generic message if possible.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>update</code>
</td>
<td>
<code><span title="telegram.Update">Update</span> | None</code>
</td>
<td>
<div class="doc-md-description">
<p>Update that caused the error (may be None).</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
<tr class="doc-section-item">
<td>
<code>context</code>
</td>
<td>
<code><span title="telegram.ext.ContextTypes.DEFAULT_TYPE">DEFAULT_TYPE</span></code>
</td>
<td>
<div class="doc-md-description">
<p>Callback context.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/handlers/errors.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span>
<span class="normal">21</span>
<span class="normal">22</span>
<span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">async</span> <span class="k">def</span><span class="w"> </span><span class="nf">error_handler</span><span class="p">(</span>
<span class="n">update</span><span class="p">:</span> <span class="n">Update</span> <span class="o">|</span> <span class="kc">None</span><span class="p">,</span> <span class="n">context</span><span class="p">:</span> <span class="n">ContextTypes</span><span class="o">.</span><span class="n">DEFAULT_TYPE</span>
<span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Global error handler: log exception and reply with generic message if possible.</span>
<span class="sd"> Args:</span>
<span class="sd"> update: Update that caused the error (may be None).</span>
<span class="sd"> context: Callback context.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">logger</span><span class="o">.</span><span class="n">exception</span><span class="p">(</span><span class="s2">&quot;Exception while handling an update&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="n">Update</span><span class="p">)</span> <span class="ow">and</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_message</span><span class="p">:</span>
<span class="n">user</span> <span class="o">=</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">update</span><span class="p">,</span> <span class="s2">&quot;effective_user&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">)</span>
<span class="n">lang</span> <span class="o">=</span> <span class="n">get_lang</span><span class="p">(</span><span class="n">user</span><span class="p">)</span> <span class="k">if</span> <span class="n">user</span> <span class="k">else</span> <span class="n">config</span><span class="o">.</span><span class="n">DEFAULT_LANGUAGE</span>
<span class="k">await</span> <span class="n">update</span><span class="o">.</span><span class="n">effective_message</span><span class="o">.</span><span class="n">reply_text</span><span class="p">(</span><span class="n">t</span><span class="p">(</span><span class="n">lang</span><span class="p">,</span> <span class="s2">&quot;errors.generic&quot;</span><span class="p">))</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div><h2 id="importers">Importers</h2>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.importers" class="doc doc-heading">
<code>duty_teller.importers</code>
</h2>
<div class="doc doc-contents first">
<p>Importers for duty data (e.g. duty-schedule JSON).</p>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-module">
<h2 id="duty_teller.importers.duty_schedule" class="doc doc-heading">
<code>duty_teller.importers.duty_schedule</code>
</h2>
<div class="doc doc-contents first">
<p>Parser for duty-schedule JSON format. No DB access.</p>
<div class="doc doc-children">
<div class="doc doc-object doc-class">
<h3 id="duty_teller.importers.duty_schedule.DutyScheduleEntry" class="doc doc-heading">
<code>DutyScheduleEntry</code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-dataclass"><code>dataclass</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>One person's schedule: full_name and three lists of dates by event type.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/importers/duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">13</span>
<span class="normal">14</span>
<span class="normal">15</span>
<span class="normal">16</span>
<span class="normal">17</span>
<span class="normal">18</span>
<span class="normal">19</span>
<span class="normal">20</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@dataclass</span>
<span class="k">class</span><span class="w"> </span><span class="nc">DutyScheduleEntry</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;One person&#39;s schedule: full_name and three lists of dates by event type.&quot;&quot;&quot;</span>
<span class="n">full_name</span><span class="p">:</span> <span class="nb">str</span>
<span class="n">duty_dates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">date</span><span class="p">]</span>
<span class="n">unavailable_dates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">date</span><span class="p">]</span>
<span class="n">vacation_dates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">date</span><span class="p">]</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.importers.duty_schedule.DutyScheduleParseError" class="doc doc-heading">
<code>DutyScheduleParseError</code>
</h3>
<div class="doc doc-contents ">
<p class="doc doc-class-bases">
Bases: <code><span title="Exception">Exception</span></code></p>
<p>Invalid or missing fields in duty-schedule JSON.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/importers/duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">32</span>
<span class="normal">33</span>
<span class="normal">34</span>
<span class="normal">35</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">class</span><span class="w"> </span><span class="nc">DutyScheduleParseError</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Invalid or missing fields in duty-schedule JSON.&quot;&quot;&quot;</span>
<span class="k">pass</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
<div class="doc doc-object doc-class">
<h3 id="duty_teller.importers.duty_schedule.DutyScheduleResult" class="doc doc-heading">
<code>DutyScheduleResult</code>
<span class="doc doc-labels">
<small class="doc doc-label doc-label-dataclass"><code>dataclass</code></small>
</span>
</h3>
<div class="doc doc-contents ">
<p>Parsed duty schedule: start_date, end_date, and per-person entries.</p>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/importers/duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal">23</span>
<span class="normal">24</span>
<span class="normal">25</span>
<span class="normal">26</span>
<span class="normal">27</span>
<span class="normal">28</span>
<span class="normal">29</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="nd">@dataclass</span>
<span class="k">class</span><span class="w"> </span><span class="nc">DutyScheduleResult</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Parsed duty schedule: start_date, end_date, and per-person entries.&quot;&quot;&quot;</span>
<span class="n">start_date</span><span class="p">:</span> <span class="n">date</span>
<span class="n">end_date</span><span class="p">:</span> <span class="n">date</span>
<span class="n">entries</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">DutyScheduleEntry</span><span class="p">]</span>
</code></pre></div></td></tr></table></div>
</details>
<div class="doc doc-children">
</div>
</div>
</div>
<div class="doc doc-object doc-function">
<h3 id="duty_teller.importers.duty_schedule.parse_duty_schedule" class="doc doc-heading">
<code class="highlight language-python"><span class="n">parse_duty_schedule</span><span class="p">(</span><span class="n">raw_bytes</span><span class="p">)</span></code>
</h3>
<div class="doc doc-contents ">
<p>Parse duty-schedule JSON into DutyScheduleResult.</p>
<p>Expects meta.start_date (YYYY-MM-DD) and schedule (array). For each schedule
item: name (required), duty string with ';' separator; index i = start_date + i days.
Cell values: в/В/б/Б =&gt; duty, Н =&gt; unavailable, О =&gt; vacation; rest ignored.</p>
<p><span class="doc-section-title">Parameters:</span></p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Description</th>
<th>Default</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code>raw_bytes</code>
</td>
<td>
<code><span title="bytes">bytes</span></code>
</td>
<td>
<div class="doc-md-description">
<p>UTF-8 encoded JSON bytes.</p>
</div>
</td>
<td>
<em>required</em>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Returns:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="DutyScheduleResult
dataclass
(duty_teller.importers.duty_schedule.DutyScheduleResult)" href="#duty_teller.importers.duty_schedule.DutyScheduleResult">DutyScheduleResult</a></code>
</td>
<td>
<div class="doc-md-description">
<p>DutyScheduleResult with start_date, end_date, and entries (per-person dates).</p>
</div>
</td>
</tr>
</tbody>
</table>
<p><span class="doc-section-title">Raises:</span></p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr class="doc-section-item">
<td>
<code><a class="autorefs autorefs-internal" title="DutyScheduleParseError (duty_teller.importers.duty_schedule.DutyScheduleParseError)" href="#duty_teller.importers.duty_schedule.DutyScheduleParseError">DutyScheduleParseError</a></code>
</td>
<td>
<div class="doc-md-description">
<p>On invalid JSON, missing/invalid meta or schedule,
or invalid item fields.</p>
</div>
</td>
</tr>
</tbody>
</table>
<details class="mkdocstrings-source">
<summary>Source code in <code>duty_teller/importers/duty_schedule.py</code></summary>
<div class="highlight"><table class="highlighttable"><tr><td class="linenos"><div class="linenodiv"><pre><span></span><span class="normal"> 38</span>
<span class="normal"> 39</span>
<span class="normal"> 40</span>
<span class="normal"> 41</span>
<span class="normal"> 42</span>
<span class="normal"> 43</span>
<span class="normal"> 44</span>
<span class="normal"> 45</span>
<span class="normal"> 46</span>
<span class="normal"> 47</span>
<span class="normal"> 48</span>
<span class="normal"> 49</span>
<span class="normal"> 50</span>
<span class="normal"> 51</span>
<span class="normal"> 52</span>
<span class="normal"> 53</span>
<span class="normal"> 54</span>
<span class="normal"> 55</span>
<span class="normal"> 56</span>
<span class="normal"> 57</span>
<span class="normal"> 58</span>
<span class="normal"> 59</span>
<span class="normal"> 60</span>
<span class="normal"> 61</span>
<span class="normal"> 62</span>
<span class="normal"> 63</span>
<span class="normal"> 64</span>
<span class="normal"> 65</span>
<span class="normal"> 66</span>
<span class="normal"> 67</span>
<span class="normal"> 68</span>
<span class="normal"> 69</span>
<span class="normal"> 70</span>
<span class="normal"> 71</span>
<span class="normal"> 72</span>
<span class="normal"> 73</span>
<span class="normal"> 74</span>
<span class="normal"> 75</span>
<span class="normal"> 76</span>
<span class="normal"> 77</span>
<span class="normal"> 78</span>
<span class="normal"> 79</span>
<span class="normal"> 80</span>
<span class="normal"> 81</span>
<span class="normal"> 82</span>
<span class="normal"> 83</span>
<span class="normal"> 84</span>
<span class="normal"> 85</span>
<span class="normal"> 86</span>
<span class="normal"> 87</span>
<span class="normal"> 88</span>
<span class="normal"> 89</span>
<span class="normal"> 90</span>
<span class="normal"> 91</span>
<span class="normal"> 92</span>
<span class="normal"> 93</span>
<span class="normal"> 94</span>
<span class="normal"> 95</span>
<span class="normal"> 96</span>
<span class="normal"> 97</span>
<span class="normal"> 98</span>
<span class="normal"> 99</span>
<span class="normal">100</span>
<span class="normal">101</span>
<span class="normal">102</span>
<span class="normal">103</span>
<span class="normal">104</span>
<span class="normal">105</span>
<span class="normal">106</span>
<span class="normal">107</span>
<span class="normal">108</span>
<span class="normal">109</span>
<span class="normal">110</span>
<span class="normal">111</span>
<span class="normal">112</span>
<span class="normal">113</span>
<span class="normal">114</span>
<span class="normal">115</span>
<span class="normal">116</span>
<span class="normal">117</span>
<span class="normal">118</span>
<span class="normal">119</span>
<span class="normal">120</span>
<span class="normal">121</span>
<span class="normal">122</span>
<span class="normal">123</span></pre></div></td><td class="code"><div><pre><span></span><code><span class="k">def</span><span class="w"> </span><span class="nf">parse_duty_schedule</span><span class="p">(</span><span class="n">raw_bytes</span><span class="p">:</span> <span class="nb">bytes</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">DutyScheduleResult</span><span class="p">:</span>
<span class="w"> </span><span class="sd">&quot;&quot;&quot;Parse duty-schedule JSON into DutyScheduleResult.</span>
<span class="sd"> Expects meta.start_date (YYYY-MM-DD) and schedule (array). For each schedule</span>
<span class="sd"> item: name (required), duty string with &#39;;&#39; separator; index i = start_date + i days.</span>
<span class="sd"> Cell values: в/В/б/Б =&gt; duty, Н =&gt; unavailable, О =&gt; vacation; rest ignored.</span>
<span class="sd"> Args:</span>
<span class="sd"> raw_bytes: UTF-8 encoded JSON bytes.</span>
<span class="sd"> Returns:</span>
<span class="sd"> DutyScheduleResult with start_date, end_date, and entries (per-person dates).</span>
<span class="sd"> Raises:</span>
<span class="sd"> DutyScheduleParseError: On invalid JSON, missing/invalid meta or schedule,</span>
<span class="sd"> or invalid item fields.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">data</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">raw_bytes</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&quot;utf-8&quot;</span><span class="p">))</span>
<span class="k">except</span> <span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">JSONDecodeError</span><span class="p">,</span> <span class="ne">UnicodeDecodeError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Invalid JSON or encoding: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> <span class="kn">from</span><span class="w"> </span><span class="nn">e</span>
<span class="n">meta</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;meta&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">meta</span> <span class="ow">or</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">meta</span><span class="p">,</span> <span class="nb">dict</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;Missing or invalid &#39;meta&#39;&quot;</span><span class="p">)</span>
<span class="n">start_str</span> <span class="o">=</span> <span class="n">meta</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;start_date&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">start_str</span> <span class="ow">or</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">start_str</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;Missing or invalid meta.start_date&quot;</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">start_date</span> <span class="o">=</span> <span class="n">date</span><span class="o">.</span><span class="n">fromisoformat</span><span class="p">(</span><span class="n">start_str</span><span class="o">.</span><span class="n">strip</span><span class="p">())</span>
<span class="k">except</span> <span class="ne">ValueError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Invalid meta.start_date: </span><span class="si">{</span><span class="n">start_str</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span> <span class="kn">from</span><span class="w"> </span><span class="nn">e</span>
<span class="n">schedule</span> <span class="o">=</span> <span class="n">data</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;schedule&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">schedule</span><span class="p">,</span> <span class="nb">list</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;Missing or invalid &#39;schedule&#39; (must be array)&quot;</span><span class="p">)</span>
<span class="n">max_days</span> <span class="o">=</span> <span class="mi">0</span>
<span class="n">entries</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">DutyScheduleEntry</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">schedule</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">row</span><span class="p">,</span> <span class="nb">dict</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;schedule item must be an object&quot;</span><span class="p">)</span>
<span class="n">name</span> <span class="o">=</span> <span class="n">row</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;name&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">name</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">or</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;schedule item must have &#39;name&#39; (string)&quot;</span><span class="p">)</span>
<span class="n">full_name</span> <span class="o">=</span> <span class="n">name</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">full_name</span><span class="p">:</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;schedule item &#39;name&#39; cannot be empty&quot;</span><span class="p">)</span>
<span class="n">duty_str</span> <span class="o">=</span> <span class="n">row</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&quot;duty&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">duty_str</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">duty_str</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">duty_str</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="k">raise</span> <span class="n">DutyScheduleParseError</span><span class="p">(</span><span class="s2">&quot;schedule item &#39;duty&#39; must be string&quot;</span><span class="p">)</span>
<span class="n">cells</span> <span class="o">=</span> <span class="p">[</span><span class="n">c</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">duty_str</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&quot;;&quot;</span><span class="p">)]</span>
<span class="n">max_days</span> <span class="o">=</span> <span class="nb">max</span><span class="p">(</span><span class="n">max_days</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">cells</span><span class="p">))</span>
<span class="n">duty_dates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">date</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">unavailable_dates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">date</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="n">vacation_dates</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">date</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">cell</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">cells</span><span class="p">):</span>
<span class="n">d</span> <span class="o">=</span> <span class="n">start_date</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="n">i</span><span class="p">)</span>
<span class="k">if</span> <span class="n">cell</span> <span class="ow">in</span> <span class="n">DUTY_MARKERS</span><span class="p">:</span>
<span class="n">duty_dates</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">cell</span> <span class="o">==</span> <span class="n">UNAVAILABLE_MARKER</span><span class="p">:</span>
<span class="n">unavailable_dates</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="k">elif</span> <span class="n">cell</span> <span class="o">==</span> <span class="n">VACATION_MARKER</span><span class="p">:</span>
<span class="n">vacation_dates</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">d</span><span class="p">)</span>
<span class="n">entries</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="n">DutyScheduleEntry</span><span class="p">(</span>
<span class="n">full_name</span><span class="o">=</span><span class="n">full_name</span><span class="p">,</span>
<span class="n">duty_dates</span><span class="o">=</span><span class="n">duty_dates</span><span class="p">,</span>
<span class="n">unavailable_dates</span><span class="o">=</span><span class="n">unavailable_dates</span><span class="p">,</span>
<span class="n">vacation_dates</span><span class="o">=</span><span class="n">vacation_dates</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">max_days</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
<span class="n">end_date</span> <span class="o">=</span> <span class="n">start_date</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">end_date</span> <span class="o">=</span> <span class="n">start_date</span> <span class="o">+</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="n">max_days</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
<span class="k">return</span> <span class="n">DutyScheduleResult</span><span class="p">(</span><span class="n">start_date</span><span class="o">=</span><span class="n">start_date</span><span class="p">,</span> <span class="n">end_date</span><span class="o">=</span><span class="n">end_date</span><span class="p">,</span> <span class="n">entries</span><span class="o">=</span><span class="n">entries</span><span class="p">)</span>
</code></pre></div></td></tr></table></div>
</details>
</div>
</div>
</div>
</div>
</div>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": [], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../assets/javascripts/bundle.79ae519e.min.js"></script>
</body>
</html>