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.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¶
holper.iofxml3 module¶
Representation of the IOF Data Standard, version 3.0 as pydantic-xml models.
We cannot parse Extensions, because they might contain arbitrary data.
pydantic-xml does not support forward refs with from __future__ import annotations, so we define all models in roughly inverted order from the xml schema.
Cf. https://github.com/international-orienteering-federation/datastandard-v3/blob/master/IOF.xsd
- class holper.iof.ClassList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, classes: ~typing.Annotated[list[~holper.iof.common.Class], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Class, ns=None, nsmap=None, nillable=None, wrapped=None)] = [])¶
Bases:
BaseMessageElementA list of classes.
- classes: ELEMENT: 1>, path='Class', ns=None, nsmap=None, nillable=None, wrapped=None)])]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class holper.iof.CompetitorList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, competitors: ~typing.Annotated[list[~holper.iof.competitor_list.Competitor], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Competitor, ns=None, nsmap=None, nillable=None, wrapped=None)] = [])¶
Bases:
BaseMessageElementA list of competitors. This is used to exchange a “brutto” list of possible competitors. This should not be used to exchange entries; use EntryList instead.
- competitors: ELEMENT: 1>, path='Competitor', ns=None, nsmap=None, nillable=None, wrapped=None)])]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class holper.iof.ControlCardList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, owner: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Owner, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The owner of the control cards.')] = None, control_cards: ~typing.Annotated[list[~holper.iof.common.ControlCard], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=ControlCard, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The control cards.')])¶
Bases:
BaseMessageElementDefines control card ownership, e.g. for rental control card handling purposes.
- control_cards: ')]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- owner: ')]¶
- class holper.iof.CourseData(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, event: ~typing.Annotated[~holper.iof.common.Event, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Event, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The event that the course data belongs to.')], race_course_data: ~typing.Annotated[list[~holper.iof.course_data.RaceCourseData], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=RaceCourseData, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The course data for each race; one element per race in the event.')])¶
Bases:
BaseMessageElementThis element defines all the control and course information for an event or race. Used when transferring courses from course-setting software to event administration software.
- delete_unused_courses() None¶
- event: ')]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- property ordered_race_course_data: list[RaceCourseData | None]¶
Order race course data by race_number.
Creates a sparse list. If an element does not have a race_number, it is put at the first empty position.
- race_course_data: ')]¶
- class holper.iof.EntryList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, event: ~typing.Annotated[~holper.iof.common.Event, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Event, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The event that the entry list belongs to.')], team_entries: ~typing.Annotated[list[~holper.iof.entry_list.TeamEntry], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=TeamEntry, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The teams registered for the event.')] = [], person_entries: ~typing.Annotated[list[~holper.iof.entry_list.PersonEntry], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=PersonEntry, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The individual competitors registered for the event.')] = [])¶
Bases:
BaseMessageElementA list of persons and/or teams which are registered for a particular event.
- event: ')]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- person_entries: ')]¶
- team_entries: ')]¶
- class holper.iof.EventList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, events: ~typing.Annotated[list[~holper.iof.common.Event], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Event, ns=None, nsmap=None, nillable=None, wrapped=None)] = [])¶
Bases:
BaseMessageElementA list of events. This can be used to exchange fixtures.
- events: ELEMENT: 1>, path='Event', ns=None, nsmap=None, nillable=None, wrapped=None)])]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class holper.iof.Importer(root: BaseMessageElement, countries: list[Country])¶
Bases:
object- extract_id(id_: Id) IdArgs¶
- find_by_id(entities: list[EntityWithId], id_: Id) EntityWithId¶
- import_class(class_: Class) EventCategory¶
- import_class_course_assignment(assignment: ClassCourseAssignment, courses: list[Course], categories: list[Category], *, use_short_category_name: bool = False) CategoryCourseAssignment | None¶
- import_control_card(control_card: ControlCard) ControlCard¶
- import_organisation(organisation: Organisation | None) Organisation | None¶
- import_person_entry(person_entry: PersonEntry, categories: list[EventCategory]) Entry¶
- import_start_time_allocation_request(request: StartTimeAllocationRequest) StartTimeAllocationRequest¶
- import_team_entry(entry: TeamEntry, categories: list[EventCategory]) Entry¶
- import_team_entry_person(entry: TeamEntryPerson) Competitor¶
- class holper.iof.OrganisationList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, organisations: ~typing.Annotated[list[~holper.iof.common.Organisation], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Organisation, ns=None, nsmap=None, nillable=None, wrapped=None)] = [])¶
Bases:
BaseMessageElementA list of organisations, including address and contact information.
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- organisations: ELEMENT: 1>, path='Organisation', ns=None, nsmap=None, nillable=None, wrapped=None)])]¶
- class holper.iof.ResultList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, event: ~typing.Annotated[~holper.iof.common.Event, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Event, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The event that the result lists belong to.')], class_results: ~typing.Annotated[list[~holper.iof.result_list.ClassResult], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=ClassResult, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('Result lists for the classes in the event.')] = [], status: ~typing.Annotated[~typing.Literal['Complete', 'Delta', 'Snapshot'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=status, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('\n The status of the result list.\n\n Complete: The result list is complete, i.e. all competitors are included. Used for official results after the event.\n Delta: The result list only contains changes since last list. Used for frequent exchange of results.\n Snapshot: The result list is a snapshot of the current standings. Used while the event is under way.")] = "Complete')])¶
Bases:
BaseMessageElementContains information about the result lists for the classes in an event.
- class_results: ')]¶
- event: ')]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- status: ")] = "Complete')]¶
- class holper.iof.ServiceRequestList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, event: ~typing.Annotated[~holper.iof.common.Event, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Event, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The event that the service requests are valid for.')], organisation_service_requests: ~typing.Annotated[list[~holper.iof.service_request_list.OrganisationServiceRequest], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=OrganisationServiceRequest, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('Service requests made by organisations.')] = [], person_service_requests: ~typing.Annotated[list[~holper.iof.service_request_list.PersonServiceRequest], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=PersonServiceRequest, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('Service requests made by persons.')] = [])¶
Bases:
BaseMessageElementA list of service requests.
- event: ')]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- organisation_service_requests: ')]¶
- person_service_requests: ')]¶
- class holper.iof.StartList(*, iof_version: ~typing.Annotated[~typing.Literal['3.0'], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=iofVersion, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The version of the IOF Interface Standard that the file conforms to.')], create_time: ~typing.Annotated[~datetime.datetime | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=createTime, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The time when the file was created.')] = None, creator: ~typing.Annotated[str | None, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ATTRIBUTE: 2>, path=creator, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The name of the software that created the file.')] = None, event: ~typing.Annotated[~holper.iof.common.Event, ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=Event, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('The event that the start lists belong to.')], class_starts: ~typing.Annotated[list[~holper.iof.start_list.ClassStart], ~pydantic_xml.fields.XmlEntityInfo(location=<EntityLocation.ELEMENT: 1>, path=ClassStart, ns=None, nsmap=None, nillable=None, wrapped=None), Doc('Start lists for the classes in the event.')] = [])¶
Bases:
BaseMessageElementContains information about the start lists for the classes in an event.
- class_starts: ')]¶
- event: ')]¶
- model_config: ClassVar[ConfigDict] = {}¶
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
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.MetaDatacollection that will be used for new_schema.Tableobjects.See also
orm_declarative_metadata
- registry: ClassVar[_RegistryType] = <sqlalchemy.orm.decl_api.registry object>¶
Refers to the
_orm.registryin use where new_orm.Mapperobjects will be associated.
- class holper.model.Category(**kwargs)¶
Bases:
BaseRealize an EventCategory for one specific race of that event
- category_id: Mapped[int]¶
- competitor_start_order: Mapped[StartOrder]¶
- 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]¶
- score: Mapped[float | None]¶
- 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]¶
- course_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(*values)¶
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]¶
- 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,HasExternalIdsLargest 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,HasExternalIdsCategory 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(*values)¶
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(*values)¶
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(*values)¶
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(*values)¶
Bases:
StrEnum- EMIT = 'emit'¶
- SPORT_IDENT = 'sport_ident'¶
- class holper.model.Race(**kwargs)¶
Bases:
BaseSmallest organisational unit to assign entries to
- event_id: Mapped[int]¶
- first_start: Mapped[datetime | None]¶
- race_id: Mapped[int]¶
- class holper.model.RaceCategoryStatus(*values)¶
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(*values)¶
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.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]¶
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]¶
Read a SportSoftware OE2010 csv export file
- read_solo_v12(input_file: IO[bytes], *, with_seconds: bool = True, encoding: str = 'latin1') Generator[Entry]¶
Read a SportSoftware OE12 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_solo_v12(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_time(string: str, *, with_seconds: bool = True) 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:
ExceptionRaise 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:
objectDeclare constraints for start list creation
- property course_slot_counts: dict[int, int]¶
- class holper.start.StartModel(constraints: StartConstraints, *, time_max: int | None = None, interval_max: int = 12, interval_common_factor: int = 1)¶
Bases:
CpModel
- holper.start.fill_slots(race: Race, constraints: StartConstraints, start_slots: Mapping[int, Iterable[int]]) None¶
Assign
entriesto the start slots.There already has to be a
Startobject which defines the category the entry is assigned to.
- holper.start.generate_slots_greedily(constraints: StartConstraints, time_max: int = 720) dict[int, 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, 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