holper package¶
Submodules¶
holper.affine_seq module¶
Affine Sequences with convenience operators
- class holper.affine_seq.AffineSeq(start: int | str, stop: int, step: int = 1)¶
Bases:
object
- pretty() str ¶
- start¶
- step¶
- stop¶
- to_range() range ¶
- holper.affine_seq.lcm(int1: int, int2: int) int ¶
Calculate the least common multiple of two integers
holper.core module¶
Core functionality
- holper.core.get_countries(session: Session) list[holper.model.Country] ¶
- holper.core.get_event(session: Session, event_id: int) holper.model.Event | None ¶
- holper.core.get_race(session: Session, race_id: int) holper.model.Race | None ¶
- holper.core.group_courses_by_first_control(race: Race) dict[str, list[holper.model.Course]] ¶
- holper.core.open_session(source: str) Session ¶
holper.invoice module¶
Classes to manage invoices
- class holper.invoice.Group¶
Bases:
TypedDict
- amount: int¶
- description: str¶
- price: Decimal¶
- total: Decimal¶
- class holper.invoice.Invoice(prices: Prices, recipient: str, date_format: str = '%y-%m-%d')¶
Bases:
object
- add_items(item_id: str, amount: int = 1) None ¶
- check_total(total: float) bool ¶
- fill_template(template_file: Path, target_file: Path, labels: dict[str, str], *, reserve_space: bool = False) None ¶
- get_total() Decimal ¶
- set_paid(amount: float) None ¶
- set_remark(remark: str) None ¶
- class holper.invoice.Prices¶
Bases:
object
- add_price(item_id: str, price: float, description: str, group: str | None = None) None ¶
- get_description(item_id: str) str ¶
- get_group_name(item_id: str) str | None ¶
- get_price(item_id: str) Decimal ¶
- holper.invoice.round_amount(number: float | decimal.Decimal) Decimal ¶
holper.iofxml3 module¶
holper.model module¶
Data Model
Define the central data types as stored in an event database. This is based on the data types defined in IOF XML v3.0. Use SQLAlchemy as database library.
- class holper.model.Base(**kwargs: Any)¶
Bases:
DeclarativeBase
- property id_column: str¶
Convention for the primary id column
- metadata: ClassVar[MetaData] = MetaData()¶
Refers to the
_schema.MetaData
collection that will be used for new_schema.Table
objects.See also
orm_declarative_metadata
- registry: ClassVar[_RegistryType] = <sqlalchemy.orm.decl_api.registry object>¶
Refers to the
_orm.registry
in use where new_orm.Mapper
objects will be associated.
- class holper.model.Category(**kwargs)¶
Bases:
Base
Realize an EventCategory for one specific race of that event
- category_id: Mapped[int]¶
- courses: Mapped[list[CategoryCourseAssignment]]¶
- event_category: Mapped[EventCategory]¶
- event_category_id: Mapped[int]¶
- property name: str¶
- race_id: Mapped[int]¶
- property short_name: str¶
- starter_limit: Mapped[int | None]¶
- status: Mapped[RaceCategoryStatus]¶
- time_offset: Mapped[timedelta | None]¶
Start time offset from race start time
- vacancies_after: Mapped[int]¶
- vacancies_before: Mapped[int]¶
- class holper.model.Competitor(**kwargs)¶
Bases:
Base
,HasExternalIds
- competitor_id: Mapped[int]¶
- control_cards: Mapped[list[ControlCard]]¶
- entry_id: Mapped[int]¶
- entry_sequence: Mapped[int]¶
1-based position of the competitor in the team
- external_ids: Mapped[list[CompetitorXID]]¶
- leg_number: Mapped[int | None]¶
- leg_order: Mapped[int | None]¶
- organisation: Mapped[Organisation | None]¶
- organisation_id: Mapped[int | None]¶
- person_id: Mapped[int]¶
- starts: Mapped[list[CompetitorStart]]¶
- class holper.model.CompetitorResult(**kwargs)¶
Bases:
Base
- competitor_result_id: Mapped[int]¶
- competitor_start: Mapped[CompetitorStart]¶
- finish_time: Mapped[datetime | None]¶
Actual finish time used for placement
- start_time: Mapped[datetime | None]¶
Actual start time used for placement
- status: Mapped[ResultStatus | None]¶
- time: Mapped[timedelta | None]¶
- time_adjustment: Mapped[timedelta]¶
Time bonus or penalty
- class holper.model.CompetitorStart(**kwargs)¶
Bases:
Base
- competitor: Mapped[Competitor]¶
- competitor_id: Mapped[int]¶
- competitor_result: Mapped[CompetitorResult]¶
- competitor_start_id: Mapped[int]¶
- control_card: Mapped[ControlCard | None]¶
- control_card_id: Mapped[int | None]¶
- start_id: Mapped[int]¶
- time_offset: Mapped[timedelta | None]¶
Start time offset from entry start time
- class holper.model.Control(**kwargs)¶
Bases:
Base
- control_id: Mapped[int]¶
- label: Mapped[str]¶
- race_id: Mapped[int]¶
- class holper.model.ControlCard(**kwargs)¶
Bases:
Base
- control_card_id: Mapped[int]¶
- label: Mapped[str | None]¶
- system: Mapped[PunchingSystem | None]¶
- class holper.model.ControlType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- CONTROL = 'control'¶
- CROSSING_POINT = 'crossing_point'¶
- END_OF_MARKED_ROUTE = 'end_of_marked_route'¶
- FINISH = 'finish'¶
- START = 'start'¶
- class holper.model.Country(**kwargs)¶
Bases:
Base
- country_id: Mapped[int]¶
ISO-3166 numeric code
- ioc_code: Mapped[str | None]¶
International Olympic Committee’s 3-letter code
- iso_alpha_2: Mapped[str]¶
ISO-3166 alpha-2 code
- iso_alpha_3: Mapped[str]¶
ISO-3166 alpha-3 code
- name: Mapped[str]¶
- class holper.model.Course(**kwargs)¶
Bases:
Base
- categories: Mapped[list[CategoryCourseAssignment]]¶
- climb: Mapped[float | None]¶
Course climb in meters
- controls: Mapped[list[CourseControl]]¶
- course_id: Mapped[int]¶
- length: Mapped[float | None]¶
Course length in kilometers
- name: Mapped[str]¶
- race_id: Mapped[int]¶
- class holper.model.CourseControl(**kwargs)¶
Bases:
Base
- after: Mapped[Self | None]¶
Control must be punched after this other control.
- after_course_control_id: Mapped[int | None]¶
- before: Mapped[Self | None]¶
Control must be punched before this other control.
- before_course_control_id: Mapped[int | None]¶
- control_id: Mapped[int]¶
- course_control_id: Mapped[int]¶
- course_id: Mapped[int]¶
- leg_length: Mapped[float | None]¶
Leg length in kilometers
- order: Mapped[int | None]¶
If a course control has a higher order than another, it has to be punched after it.
- score: Mapped[float | None]¶
- type: Mapped[ControlType]¶
- class holper.model.Entry(**kwargs)¶
Bases:
Base
,HasExternalIds
- category_requests: Mapped[list[EntryCategoryRequest]]¶
Requested categories with preference
- competitors: Mapped[list[Competitor]]¶
- entry_id: Mapped[int]¶
- event_id: Mapped[int]¶
- external_ids: Mapped[list[EntryXID]]¶
- name: Mapped[str | None]¶
- number: Mapped[int | None]¶
- organisation: Mapped[Organisation | None]¶
- organisation_id: Mapped[int | None]¶
- property races: list[holper.model.Race]¶
- start_time_allocation_requests: Mapped[list[StartTimeAllocationRequest]]¶
- class holper.model.EntryCategoryRequest(**kwargs)¶
Bases:
Base
- entry_id: Mapped[int]¶
- event_category: Mapped[EventCategory]¶
- event_category_id: Mapped[int]¶
- preference: Mapped[int]¶
Lower number means higher preference
- class holper.model.Event(**kwargs)¶
Bases:
Base
,HasExternalIds
Largest organisational unit to assign entries to
An event can consist of one or multiple
races
. This occurs for multi-day events and for events with heats and finals. All races of an event must have the same form (e.g. individual or relay) and offer the same categories defined byEventCategory
.- end_time: Mapped[datetime | None]¶
- event_categories: Mapped[list[EventCategory]]¶
- event_id: Mapped[int]¶
- name: Mapped[str | None]¶
- start_time: Mapped[datetime | None]¶
- class holper.model.EventCategory(**kwargs)¶
Bases:
Base
,HasExternalIds
Category in an event
Here category information specific to an event but common to all races is stored.
For example included are a number of legs. Usually this occurs for relay events, but can also be used when each starter must complete several courses, such that the total time is used for the final ranking.
- entry_requests: Mapped[list[EntryCategoryRequest]]¶
- event_category_id: Mapped[int]¶
- event_id: Mapped[int | None]¶
- external_ids: Mapped[list[EventCategoryXID]]¶
- max_age: Mapped[int | None]¶
- max_number_of_team_members: Mapped[int]¶
- max_team_age: Mapped[int | None]¶
- min_age: Mapped[int | None]¶
- min_number_of_team_members: Mapped[int]¶
- min_team_age: Mapped[int | None]¶
- name: Mapped[str]¶
- short_name: Mapped[str | None]¶
- starter_limit: Mapped[int | None]¶
- status: Mapped[EventCategoryStatus]¶
- class holper.model.EventCategoryStatus(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- DIVIDED = 'divided'¶
- INVALIDATED = 'invalidated'¶
- INVALIDATED_NO_FEE = 'invalidated_no_fee'¶
- JOINED = 'joined'¶
- NORMAL = 'normal'¶
- class holper.model.EventCategoryXID(**kwargs)¶
Bases:
Base
,ExternalId
- event_category: Mapped[EventCategory]¶
- event_category_id: Mapped[int]¶
- external_id¶
- issuer¶
- class holper.model.EventForm(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- INDIVIDUAL = 'individual'¶
- RELAY = 'relay'¶
- TEAM = 'team'¶
- class holper.model.EventXID(**kwargs)¶
Bases:
Base
,ExternalId
- event_id: Mapped[int]¶
- external_id¶
- issuer¶
- class holper.model.Leg(**kwargs)¶
Bases:
Base
- event_category: Mapped[EventCategory]¶
- event_category_id: Mapped[int]¶
- leg_id: Mapped[int]¶
- leg_number: Mapped[int | None]¶
- max_number_of_competitors: Mapped[int]¶
- min_number_of_competitors: Mapped[int]¶
- class holper.model.Organisation(**kwargs)¶
Bases:
Base
,HasExternalIds
- country_id: Mapped[int | None]¶
- external_ids: Mapped[list[OrganisationXID]]¶
- name: Mapped[str]¶
- organisation_id: Mapped[int]¶
- short_name: Mapped[str | None]¶
- type: Mapped[OrganisationType | None]¶
- class holper.model.OrganisationType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- CLUB = 'club'¶
- COMPANY = 'company'¶
- IOF = 'iof'¶
- IOF_REGION = 'iof_region'¶
- MILITARY = 'military'¶
- NATIONAL_FEDERATION = 'national_federation'¶
- NATIONAL_REGION = 'national_region'¶
- OTHER = 'other'¶
- SCHOOL = 'school'¶
- class holper.model.OrganisationXID(**kwargs)¶
Bases:
Base
,ExternalId
- external_id¶
- issuer¶
- organisation: Mapped[Organisation]¶
- organisation_id: Mapped[int]¶
- class holper.model.Person(**kwargs)¶
Bases:
Base
,HasExternalIds
- birth_date: Mapped[date | None]¶
- country_id: Mapped[int | None]¶
- family_name: Mapped[str | None]¶
- given_name: Mapped[str | None]¶
- person_id: Mapped[int]¶
- title: Mapped[str | None]¶
- class holper.model.PersonXID(**kwargs)¶
Bases:
Base
,ExternalId
- external_id¶
- issuer¶
- person_id: Mapped[int]¶
- class holper.model.PunchingSystem(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- EMIT = 'emit'¶
- SPORT_IDENT = 'sport_ident'¶
- class holper.model.Race(**kwargs)¶
Bases:
Base
Smallest organisational unit to assign entries to
- property entries: list[holper.model.Entry]¶
- event_id: Mapped[int]¶
- first_start: Mapped[datetime | None]¶
- race_id: Mapped[int]¶
- class holper.model.RaceCategoryStatus(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- COMPLETED = 'completed'¶
- INVALIDATED = 'invalidated'¶
- INVALIDATED_NO_FEE = 'invalidated_no_fee'¶
- NOT_USED = 'not_used'¶
- START_TIMES_ALLOCATED = 'start_times_allocated'¶
- START_TIMES_NOT_ALLOCATED = 'start_times_not_allocated'¶
- class holper.model.Result(**kwargs)¶
Bases:
Base
- finish_time: Mapped[datetime | None]¶
Actual finish time used for placement
- position: Mapped[int | None]¶
Position in the category
- result_id: Mapped[int]¶
- start_time: Mapped[datetime | None]¶
Actual start time used for placement
- status: Mapped[ResultStatus | None]¶
- time: Mapped[timedelta | None]¶
- time_adjustment: Mapped[timedelta]¶
Time bonus or penalty
- class holper.model.ResultStatus(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- ACTIVE = 'active'¶
- CANCELLED = 'cancelled'¶
- DID_NOT_ENTER = 'did_not_enter'¶
- DID_NOT_FINISH = 'did_not_finish'¶
- DID_NOT_START = 'did_not_start'¶
- DISQUALIFIED = 'disqualified'¶
- FINISHED = 'finished'¶
- INACTIVE = 'inactive'¶
- MISSING_PUNCH = 'missing_punch'¶
- MOVED = 'moved'¶
- MOVED_UP = 'moved_up'¶
- NOT_COMPETING = 'not_competing'¶
- OK = 'ok'¶
- OVER_TIME = 'over_time'¶
- SPORTING_WITHDRAWAL = 'sporting_withdrawal'¶
- class holper.model.Sex(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
StrEnum
- FEMALE = 'female'¶
- MALE = 'male'¶
- class holper.model.Start(**kwargs)¶
Bases:
Base
- category_id: Mapped[int]¶
- competitive: Mapped[bool]¶
Whether the starter is to be considered for the official ranking. This can be set to False for example if the starter does not fulfill some entry requirement.
- competitor_starts: Mapped[list[CompetitorStart]]¶
- entry_id: Mapped[int]¶
- start_id: Mapped[int]¶
- time_offset: Mapped[timedelta | None]¶
Start time offset from category start time
- class holper.model.StartTimeAllocationRequest(**kwargs)¶
Bases:
Base
- entry_id: Mapped[int]¶
- organisation: Mapped[Organisation | None]¶
- organisation_id: Mapped[int | None]¶
- person_id: Mapped[int | None]¶
- start_time_allocation_request_id: Mapped[int]¶
- type: Mapped[StartTimeAllocationRequestType]¶
holper.sportsoftware module¶
Data exchange in Krämer SportSoftware OE/OS/OT csv
- class holper.sportsoftware.CSVReader(race: Race)¶
Bases:
object
- read_category(category_id_repr: str, short_name: str, name: str, team_size_repr: str = '') Category ¶
- read_club(club_id_repr: str, abbreviation: str, city: str, country: str, _seat: str | None = None, _region: str | None = None) Organisation ¶
- read_competitor(family_name: str, given_name: str, birth_year_repr: str, sex: str, control_card_label: str) Competitor ¶
- read_relay_v11(input_file: IO[bytes], *, with_seconds: bool = True, encoding: str = 'latin1') Generator[Entry, None, None] ¶
Read a SportSoftware OS2010 csv export file
- read_result_status(status: str) ResultStatus ¶
- read_solo_v11(input_file: IO[bytes], *, with_seconds: bool = True, encoding: str = 'latin1') Generator[Entry, None, None] ¶
Read a SportSoftware OE2010 csv export file
- read_start_and_result(non_competitive: str, start_offset_repr: str, finish_offset_repr: str = '', result_time: str = '', status: str = '', time_bonus: str = '', time_penalty: str = '', *, with_seconds: bool = True) Start ¶
Read start and result columns
Note: The export data only contains relative time values, while the model expects start and finish time to be proper DateTime values. As a work-around, here we store these as DateTime types, which still need to be shifted to the proper race start time.
- class holper.sportsoftware.CSVWriter(race: Race)¶
Bases:
object
- write_category(category: EventCategory) list[str] ¶
- write_club(club: Organisation) list[str] ¶
Convert club to cells
A club id and name is required.
- write_competitor(competitor: Competitor) list[str] ¶
- write_relay_v11(output_file: IO[bytes], encoding: str = 'latin1') None ¶
- write_solo_v11(output_file: IO[bytes], encoding: str = 'latin1') None ¶
- write_team_v10(output_file: IO[bytes], encoding: str = 'latin1') None ¶
- holper.sportsoftware.detect(input_file: IO[bytes]) bool ¶
- holper.sportsoftware.format_time(value: timedelta, *, with_seconds: bool = True) str ¶
- holper.sportsoftware.parse_float(string: str) float | None ¶
- holper.sportsoftware.parse_sex(string: str) holper.model.Sex | None ¶
- holper.sportsoftware.parse_time(string: str, *, with_seconds: bool = True) datetime.timedelta | None ¶
holper.start module¶
Generate start times.
The implementation follows the official german competiton rules (WKB). That means competitors in a category have to start in equal intervals. Intervals between categories can differ though.
Multiple categories that run on the same course are required to start one after
another with one unused start slot in between. That means the gap between them
is at least two times the gap between two competitiors on the course. The
StartConstraints
class can be used to define conditions that must
be followed when generating start slots, e.g. the order of the categories with
the same course.
- exception holper.start.InfeasibleError¶
Bases:
Exception
Raise when an optimization cannot find a solution.
- class holper.start.StartConstraints(interval: int = 1, parallel_max: int | None = None, conflicts: list[list[int]] | None = None)¶
Bases:
object
Declare constraints for start list creation
- property course_slot_counts: dict[int, int]¶
- get_categories(course: Course) list[holper.model.Category] ¶
- set_category_early(course: Course, categories: list[holper.model.Category]) None ¶
- set_category_late(course: Course, categories: list[holper.model.Category]) None ¶
- holper.start.fill_slots(race: Race, constraints: StartConstraints, start_slots: Mapping[int, Iterable[int]]) None ¶
Assign
entries
to the start slots.There already has to be a
Start
object which defines the category the entry is assigned to.
- holper.start.generate_slots_greedily(constraints: StartConstraints, time_max: int = 720) dict[int, holper.affine_seq.AffineSeq] ¶
Greedily finds a start slot scheme under the given constraints
- Parameters:
constraints – Conditions that the resulting start list must follow.
time_max – Time limit before which all starts have to occur. Can usually be left as the default, because it has no impact on the calculation time.
- Returns:
Start slots object for each course id
- holper.start.generate_slots_optimal(constraints: StartConstraints, timeout: int = 30) dict[int, holper.affine_seq.AffineSeq] ¶
Tries to find the optimal compact start slot scheme
- Parameters:
constraints – Conditions that the resulting start list must follow.
timeout – Number of seconds after which to stop the optimization.
- Returns:
Start slots object for each course id
holper.tools module¶
Miscellaneous helper functions
- holper.tools.camelcase_to_snakecase(name_camel: str) str ¶
Convert CamelCase to snake_case syntax
- holper.tools.disjoin(lst: list[T], key: Callable[[T], Any]) None ¶
Disjoin similar elements of a list by reordering the list in a deterministic way.
- holper.tools.fix_sqlite_engine(engine: Engine) None ¶
Fix pysqlite, Cf.: http://docs.sqlalchemy.org/en/latest/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl
- holper.tools.normalize_year(year_repr: str) int | None ¶
Convert a possible two-digit year into a four-digit year