Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom datetime converter #4

Merged
merged 4 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Build and test

on:
push:
branches:
- main

pull_request:
branches:
- main

jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v4
chrismostert marked this conversation as resolved.
Show resolved Hide resolved
with:
python-version: "3.10"

- name: Build and install package
run: |
python -m pip install .[tests]
- name: Download testdata
run: |
wget -i testfiles.txt -P data
- name: Unzip all testdata
run: |
unzip -d data -o 'data/*.zip'
- name: Run tests
run: |
pytest
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,10 @@ To build the package yourself instead of installing the `.whl`, simply clone the
python -m build
```

or you can simply install the package by running
```
python -m pip install .
```

## Codegen
These bindings are mostly generated using [xsData](https://xsdata.readthedocs.io) with some minor changes where needed. See commit history for these changes.
82 changes: 52 additions & 30 deletions pyeml_bindings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Generator: DataclassGenerator
See: https://xsdata.readthedocs.io/
"""

from pyeml_bindings.converters.xml_date_time import FullPrecisionXmlDateTimeConverter
from pyeml_bindings.emlcore_kiesraad_strict import (
Accepted,
Affiliation,
Expand All @@ -23,8 +25,8 @@
BallotIdentifierRange,
BallotIdentifierRangeStructure,
BallotIdentifierStructure,
BinaryItemStructure,
BinaryFormat,
BinaryItemStructure,
Candidate,
CandidateIdentifier,
CandidateIdentifierStructure,
Expand All @@ -36,19 +38,19 @@
ContactDetailsStructure,
ContestIdentifier,
ContestIdentifierStructure,
CountingAlgorithm,
CountMetric,
CountMetricStructure,
CountQualifier,
CountQualifierStructure,
CountingAlgorithm,
DocumentIdentifier,
DocumentIdentifierStructure,
Emlstructure,
ElectionGroupStructure,
ElectionIdentifier,
ElectionIdentifierStructure,
ElectionStatement,
EmailStructure,
Emlstructure,
Endorsement,
EventIdentifier,
EventIdentifierStructure,
Expand All @@ -63,8 +65,8 @@
ManagingAuthority,
ManagingAuthorityStructure,
MaxVotes,
MessageType,
MessagesStructure,
MessageType,
MinVotes,
NominatingOfficer,
NominatingOfficerStructure,
Expand All @@ -74,7 +76,6 @@
Period,
PeriodStructure,
PeriodStructurePermanent,
PersonName as EmlcorePersonName,
PollingDistrict,
PollingDistrictStructure,
PollingPlace,
Expand Down Expand Up @@ -113,10 +114,6 @@
SupporterStructure,
TelephoneStructure,
TransactionId,
Vtoken,
VtokenQualified,
VtokenQualifiedStructure,
VtokenStructure,
VoterIdentificationStructure,
VoterInformationStructure,
VoterInformationStructureGender,
Expand All @@ -125,10 +122,17 @@
VotingChannelType,
VotingMethod,
VotingMethodType,
Vtoken,
VtokenQualified,
VtokenQualifiedStructure,
VtokenStructure,
WriteIn,
WriteInType,
YesNoType,
)
from pyeml_bindings.emlcore_kiesraad_strict import (
PersonName as EmlcorePersonName,
)
from pyeml_bindings.emlexternals_kiesraad_strict import (
AuthorityAddressStructure,
ElectoralAddressStructure,
Expand Down Expand Up @@ -182,8 +186,8 @@
CandidateStructureKr,
ContactDetailsStructureKr,
ContestIdentifierStructureKr,
EmlstructureKr,
ElectionIdentifierStructureKr,
EmlstructureKr,
GenericMailingAddressStructureKr,
GenericQualifyingAddressStructureKr,
MailingAddressStructureKr,
Expand All @@ -194,56 +198,66 @@
)
from pyeml_bindings.mod_110a_electionevent_kiesraad_strict import (
ContestIdentifierStructure110A,
Emlstructure110,
Eml as Eml110a,
ElectionEvent,
ElectionIdentifierStructure110A,
Emlstructure110,
PollingPlaceStructure110,
PollingPlaceStructure110Channel,
)
from pyeml_bindings.mod_110a_electionevent_kiesraad_strict import (
Eml as Eml110a,
)
from pyeml_bindings.mod_210_nomination_kiesraad_strict import (
AffiliationIdentifierStructure210,
AffiliationStructure210,
CandidateIdentifierStructure210,
CandidateStructure210,
ContestIdentifierStructure210,
Emlstructure210,
Eml as Eml210,
ElectionIdentifierStructure210,
Emlstructure210,
Nomination,
ProposerStructureKr,
ProposerStructureRestricted,
ProposerStructureRestrictedJobTitle,
)
from pyeml_bindings.mod_210_nomination_kiesraad_strict import (
Eml as Eml210,
)
from pyeml_bindings.mod_230_candidatelist_kiesraad_strict import (
CandidateList,
ElectionIdentifierStructure230,
Emlstructure230,
Eml as Eml230,
Emlstructure230Id,
ElectionIdentifierStructure230,
)
from pyeml_bindings.mod_230_candidatelist_kiesraad_strict import (
Eml as Eml230,
)
from pyeml_bindings.mod_510_count_kiesraad_strict import (
AffiliationIdentifierStructure510,
CandidateIdentifierStructure510,
CandidateStructure510,
Count,
Emlstructure510,
Eml as Eml510,
ElectionIdentifierStructure510,
Emlstructure510,
RejectedVotesReasonCode,
ReportingUnitVotes,
UncountedVotesReasonCode,
)
from pyeml_bindings.mod_510_count_kiesraad_strict import (
Eml as Eml510,
)
from pyeml_bindings.mod_520_result_kiesraad_strict import (
AffiliationIdentifierStructure520,
CandidateIdentifierStructure520,
CandidateStructure520,
Emlstructure520,
Eml as Eml520,
ElectionIdentifierStructure520,
Emlstructure520,
Result,
SelectionRanking,
)
from pyeml_bindings.mod_520_result_kiesraad_strict import (
Eml as Eml520,
)
from pyeml_bindings.x_al_kiesraad_strict import (
Address,
AddressDetails,
Expand Down Expand Up @@ -271,41 +285,41 @@
MailStopType,
MinimalCountryType,
MinimalLocalityType,
PostalCode,
PostalRouteType,
PostBox,
PostOffice,
PostOfficeNumberIndicatorOccurrence,
PostalCode,
PostalRouteType,
Premise,
PremiseNameTypeOccurrence,
PremiseNumber,
PremiseNumberIndicatorOccurrence,
PremiseNumberNumberType,
PremiseNumberNumberTypeOccurrence,
PremiseNumberPrefix,
PremiseNumberRangeIndicatorOccurence,
PremiseNumberRangeNumberRangeOccurence,
PremiseNumberSuffix,
PremiseNumberIndicatorOccurrence,
PremiseNumberNumberType,
PremiseNumberNumberTypeOccurrence,
SubPremiseNameTypeOccurrence,
SubPremiseNumberIndicatorOccurrence,
SubPremiseNumberNumberTypeOccurrence,
SubPremiseType,
Thoroughfare,
ThoroughfareDependentThoroughfares,
ThoroughfareLeadingTypeType,
ThoroughfareNameType,
ThoroughfareNumber,
ThoroughfareNumberIndicatorOccurrence,
ThoroughfareNumberNumberOccurrence,
ThoroughfareNumberNumberType,
ThoroughfareNumberPrefix,
ThoroughfareNumberRangeIndicatorOccurrence,
ThoroughfareNumberRangeNumberRangeOccurrence,
ThoroughfareNumberRangeRangeType,
ThoroughfareNumberSuffix,
ThoroughfareNumberIndicatorOccurrence,
ThoroughfareNumberNumberOccurrence,
ThoroughfareNumberNumberType,
ThoroughfarePostDirectionType,
ThoroughfarePreDirectionType,
ThoroughfareTrailingTypeType,
ThoroughfareDependentThoroughfares,
XAl,
)
from pyeml_bindings.x_nl_kiesraad_strict import (
Expand All @@ -316,9 +330,11 @@
NameLineType,
OrganisationNameDetails,
OrganisationNameDetails1,
PersonName as XNlPersonName,
XNl,
)
from pyeml_bindings.x_nl_kiesraad_strict import (
PersonName as XNlPersonName,
)

__all__ = [
"Accepted",
Expand Down Expand Up @@ -616,3 +632,9 @@
"PollingPlaceStructure110",
"PollingPlaceStructure110Channel",
]

# Register custom converters for serialization
from xsdata.formats.converter import converter
from xsdata.models.datatype import XmlDateTime

converter.register_converter(XmlDateTime, FullPrecisionXmlDateTimeConverter())
51 changes: 51 additions & 0 deletions pyeml_bindings/converters/xml_date_time.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Any, Optional

from xsdata.formats.converter import Converter
from xsdata.models.datatype import XmlDateTime
from xsdata.utils.dates import format_date, format_offset


def custom_format_time(
hour: int, minute: int, second: int, fractional_second: int
) -> str:
"""Serializes a time according to ISO 8601.

Args:
hour (int): The hour to serialize
minute (int): The minute to serialize
second (int): The second to serialize
fractional_second (int): The fractional second to serialize. Can be either nano- micro- or milliseconds.

Returns:
str: The time formatted according to ISO 8601 (example: 14:14:33.000)
"""
microsecond, nano = divmod(fractional_second, 1000)
if nano:
return f"{hour:02d}:{minute:02d}:{second:02d}.{fractional_second:09d}"

milli, micro = divmod(microsecond, 1000)
if micro:
return f"{hour:02d}:{minute:02d}:{second:02d}.{microsecond:06d}"

return f"{hour:02d}:{minute:02d}:{second:02d}.{milli:03d}"


class FullPrecisionXmlDateTimeConverter(Converter):
"""Override the default XmlDateTimeConverter to preserve full precision when serializing datetimes

Args:
Converter (xsdata.formats.converter.Converter): Abstract converter class
"""

def deserialize(self, value: Any, **kwargs: Any) -> Any:
return XmlDateTime.from_string(value)

def serialize(self, value: Any, **kwargs: Any) -> Optional[str]:
if isinstance(value, XmlDateTime):
return "{}T{}{}".format(
format_date(value.year, value.month, value.day),
custom_format_time(
value.hour, value.minute, value.second, value.fractional_second
),
format_offset(value.offset),
)
Loading
Loading