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: Mapped[Race]
race_id: Mapped[int]
property short_name: str
starter_limit: Mapped[int | None]
starts: Mapped[list[Start]]
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: Mapped[Entry]
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: Mapped[Person]
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: Mapped[Start]
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: Mapped[Race]
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: Mapped[Race]
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: Mapped[Control]
control_id: Mapped[int]
course: Mapped[Course]
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: Mapped[Event]
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]]
starts: Mapped[list[Start]]
class holper.model.EntryCategoryRequest(**kwargs)

Bases: Base

entry: Mapped[Entry]
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 by EventCategory.

end_time: Mapped[datetime | None]
entries: Mapped[list[Entry]]
event_categories: Mapped[list[EventCategory]]
event_id: Mapped[int]
external_ids: Mapped[list[EventXID]]
form: Mapped[EventForm]
name: Mapped[str | None]
races: Mapped[list[Race]]
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: Mapped[Event | None]
event_category_id: Mapped[int]
event_id: Mapped[int | None]
external_ids: Mapped[list[EventCategoryXID]]
legs: Mapped[list[Leg]]
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]
sex: Mapped[Sex | None]
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: Mapped[Event]
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: Mapped[Country | None]
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: Mapped[Country | None]
country_id: Mapped[int | None]
external_ids: Mapped[list[PersonXID]]
family_name: Mapped[str | None]
given_name: Mapped[str | None]
person_id: Mapped[int]
sex: Mapped[Sex | None]
title: Mapped[str | None]
class holper.model.PersonXID(**kwargs)

Bases: Base, ExternalId

external_id
issuer
person: Mapped[Person]
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

categories: Mapped[list[Category]]
controls: Mapped[list[Control]]
courses: Mapped[list[Course]]
property entries: list[holper.model.Entry]
event: Mapped[Event]
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: Mapped[Start]
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: Mapped[Category]
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: Mapped[Entry]
entry_id: Mapped[int]
result: Mapped[Result]
start_id: Mapped[int]
time_offset: Mapped[timedelta | None]

Start time offset from category start time

class holper.model.StartTimeAllocationRequest(**kwargs)

Bases: Base

entry: Mapped[Entry]
entry_id: Mapped[int]
organisation: Mapped[Organisation | None]
organisation_id: Mapped[int | None]
person: Mapped[Person | None]
person_id: Mapped[int | None]
start_time_allocation_request_id: Mapped[int]
type: Mapped[StartTimeAllocationRequestType]
class holper.model.StartTimeAllocationRequestType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: StrEnum

EARLY_START = 'early_start'
GROUPED_WITH = 'grouped_with'
LATE_START = 'late_start'
NORMAL = 'normal'
SEPARATED_FROM = 'separated_from'

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.

read_team_v10(input_file: IO[bytes], *, with_seconds: bool = True, encoding: str = 'latin1') Generator[Entry, None, None]

Read a SportSoftware OT2003 csv export file

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_start_and_result(start: Start) list[str]
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.sportsoftware.read(input_file: IO[bytes], encoding: str = 'latin1') Generator[Entry, None, None]
holper.sportsoftware.write(output_file: IO[bytes], race: Race, encoding: str = 'latin1') 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

add_race_courses(race: Race) None
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
class holper.start.Statistics(race: Race)

Bases: object

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

Module contents