squoredata 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
squoredata/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ """Self-contained Squore data structures.
2
+
3
+ This package encapsulates the data structures that a Squore tablet uses, as
4
+ opposed to the surrounding server logic in the ``tcboard`` project.
5
+ """
6
+
7
+ from .devinfo import BatteryStatus, DeviceInfo, SquoreDeviceInfo
8
+ from .game import Game, Result, ScoresType, ScoreType
9
+ from .livedata import (
10
+ InconsistentStateError,
11
+ LockStateType,
12
+ ServerType,
13
+ SquoreMatchLiveData,
14
+ TimerInfo,
15
+ Timing,
16
+ When,
17
+ )
18
+ from .livestatus import LiveStatus
19
+ from .match import (
20
+ Clubs,
21
+ Colors,
22
+ Countries,
23
+ Event,
24
+ Format,
25
+ Metadata,
26
+ Players,
27
+ PlayersProps,
28
+ SquoreMatch,
29
+ Wifi,
30
+ )
31
+ from .point import (
32
+ AppealType,
33
+ PlayerIndex,
34
+ PlayerLetter,
35
+ Point,
36
+ ServerSide,
37
+ make_point_from_squore_line,
38
+ )
39
+
40
+ __all__ = [
41
+ "AppealType",
42
+ "BatteryStatus",
43
+ "Clubs",
44
+ "Colors",
45
+ "Countries",
46
+ "DeviceInfo",
47
+ "Event",
48
+ "Format",
49
+ "Game",
50
+ "InconsistentStateError",
51
+ "LiveStatus",
52
+ "LockStateType",
53
+ "Metadata",
54
+ "PlayerIndex",
55
+ "PlayerLetter",
56
+ "Players",
57
+ "PlayersProps",
58
+ "Point",
59
+ "Result",
60
+ "ScoreType",
61
+ "ScoresType",
62
+ "ServerSide",
63
+ "ServerType",
64
+ "SquoreDeviceInfo",
65
+ "SquoreMatch",
66
+ "SquoreMatchLiveData",
67
+ "TimerInfo",
68
+ "Timing",
69
+ "When",
70
+ "Wifi",
71
+ "make_point_from_squore_line",
72
+ ]
squoredata/devinfo.py ADDED
@@ -0,0 +1,68 @@
1
+ # needed < 3.14 so that annotations aren't evaluated
2
+ from __future__ import annotations
3
+
4
+ from datetime import datetime
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, model_validator
8
+
9
+
10
+ class BatteryStatus(BaseModel):
11
+ """The battery status of a device."""
12
+
13
+ percentage: int
14
+ charging: bool
15
+
16
+
17
+ class DeviceInfo(BaseModel):
18
+ """Base information about a device."""
19
+
20
+ deviceid: str | None = None
21
+ timestamp: datetime | None = None
22
+
23
+ def model_post_init(self, ctx: Any) -> None:
24
+ if self.timestamp is None:
25
+ self.timestamp = (ctx or {}).get("timestamp") or datetime.now()
26
+
27
+ @property
28
+ def device(self) -> str | None:
29
+ return self.deviceid
30
+
31
+ @property
32
+ def batterystatus(self) -> BatteryStatus | None:
33
+ return None
34
+
35
+ def debug_repr(self) -> str:
36
+ return str(self)
37
+
38
+ def __str__(self) -> str:
39
+ return self.device or "(no device ID)"
40
+
41
+ def __hash__(self) -> int:
42
+ return hash(self.device)
43
+
44
+
45
+ _NONEXISTENT = object()
46
+
47
+
48
+ class SquoreDeviceInfo(DeviceInfo):
49
+ """Device information as reported by a Squore tablet."""
50
+
51
+ batteryCharging: bool
52
+ batteryPercentage: int
53
+
54
+ @model_validator(mode="before")
55
+ @classmethod
56
+ def _rename_device(cls, data: Any) -> Any:
57
+ if (
58
+ isinstance(data, dict)
59
+ and (dev := data.get("device", _NONEXISTENT)) is not _NONEXISTENT
60
+ ):
61
+ data = {k: v for k, v in data.items() if k != "device"} | {"deviceid": dev}
62
+ return data
63
+
64
+ @property
65
+ def batterystatus(self) -> BatteryStatus:
66
+ return BatteryStatus(
67
+ percentage=self.batteryPercentage, charging=self.batteryCharging
68
+ )
squoredata/game.py ADDED
@@ -0,0 +1,39 @@
1
+ """Game-level data as used by Squore."""
2
+
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, field_serializer
6
+
7
+ from .point import Point
8
+
9
+ type ScoreType = tuple[int, int]
10
+ type ScoresType = list[ScoreType]
11
+
12
+ # type Result = tuple[int, int]
13
+ type Result = list[int]
14
+
15
+
16
+ class Game(BaseModel):
17
+ """A single game within a match."""
18
+
19
+ score: ScoreType
20
+ starttime: datetime | None = None
21
+ endtime: datetime | None = None
22
+ winner: int | None = None
23
+ # Winner seems a redundant field, but it is not. A game could be live, i.e. ongoing,
24
+ # and then None means there is no winner. We could of course compute this too, but
25
+ # then we'd need to know the match format here. Let's keep it simple for now.
26
+ scoreline: list[Point] = []
27
+
28
+ @field_serializer("score", mode="plain")
29
+ def _score_as_list(self, value: ScoreType) -> Result:
30
+ # Convert the tuple to a list because otherwise json2ts generates:
31
+ #
32
+ # /**
33
+ # * @minItems 2
34
+ # * @maxItems 2
35
+ # */
36
+ # export type Result = [unknown, unknown];
37
+ #
38
+ # and thus loses the typing.
39
+ return list(value)
squoredata/livedata.py ADDED
@@ -0,0 +1,233 @@
1
+ """Live match data as reported by a Squore tablet."""
2
+
3
+ import itertools
4
+ from datetime import datetime
5
+ from typing import Any, Literal, Never
6
+
7
+ from pydantic import (
8
+ BaseModel,
9
+ Field,
10
+ computed_field,
11
+ field_serializer,
12
+ field_validator,
13
+ )
14
+
15
+ from .game import Game, ScoresType
16
+ from .livestatus import LiveStatus
17
+ from .match import SquoreMatch
18
+ from .point import PlayerLetter, Point, ServerSide, make_point_from_squore_line
19
+
20
+ type ServerType = PlayerLetter
21
+ type LockStateType = ( # src/com/doubleyellow/scoreboard/model/LockState.java
22
+ Literal["Unlocked"]
23
+ | Literal["UnlockedManual"]
24
+ | Literal["UnlockedEndOfFinalGame"]
25
+ | Literal["LockedManual"]
26
+ | Literal["LockedManualGUI"]
27
+ | Literal["LockedEndOfMatch"]
28
+ | Literal["LockedIdleTime"]
29
+ | Literal["SharedEndedMatch"]
30
+ | Literal["LockedEndOfMatchRetired"]
31
+ | Literal["LockedEndOfMatchConduct"]
32
+ | Literal["LockedEndOfMatchTimeBased"]
33
+ | None
34
+ )
35
+
36
+
37
+ class InconsistentStateError(Exception):
38
+ """The Squore state is internally inconsistent."""
39
+
40
+ def __init__(self, matchid: str, deviceid: str) -> None:
41
+ super().__init__(
42
+ f"Inconsistent Squore state (bug #120), matchid={matchid}, "
43
+ f"deviceid={deviceid}"
44
+ )
45
+ self.matchid = matchid
46
+ self.deviceid = deviceid
47
+
48
+
49
+ class When(BaseModel, extra="forbid"):
50
+ """The date and time a match is scheduled for."""
51
+
52
+ date: str
53
+ time: str
54
+
55
+
56
+ class Timing(BaseModel, extra="forbid"):
57
+ """Timing information for one game."""
58
+
59
+ start: str
60
+ end: str
61
+ offsets: list[int] | None = None
62
+
63
+
64
+ class TimerInfo(BaseModel, extra="forbid"):
65
+ """Information about a running countdown timer."""
66
+
67
+ type: (
68
+ Literal["Warmup"]
69
+ | Literal["UntilStartOfFirstGame"]
70
+ | Literal["UntilStartOfNextGame"]
71
+ )
72
+ totalSeconds: int
73
+
74
+
75
+ class SquoreMatchLiveData(SquoreMatch, extra="forbid"):
76
+ """A Squore match together with the live data reported by the tablet."""
77
+
78
+ appName: Literal["Squore"]
79
+ appPackage: Literal["com.doubleyellow.scoreboard"]
80
+ court: int | None
81
+ timestamp: datetime | None = None
82
+ customData: dict[str, Any] | None = None
83
+ gamescores: str | None = None
84
+ isGameBall: bool
85
+ isHandOut: bool
86
+ isMatchBall: bool
87
+ isVictoryFor: PlayerLetter | None = None
88
+ isUndo: bool = False
89
+ lastCall: str | None = None
90
+ lastScorer: PlayerLetter | None = None
91
+ liveScoreDeviceId: str
92
+ lockState: LockStateType = None
93
+ maxNrOfPowerPlays: int | None = None
94
+ result: str
95
+ score: list[list[str]] = Field(default_factory=list[list[str]])
96
+ server: ServerType
97
+ serveSide: ServerSide
98
+ start: str
99
+ timerInfo: TimerInfo | None = None
100
+ timing: list[Timing] = Field(default_factory=list[Timing])
101
+ when: When
102
+
103
+ def model_post_init(self, ctx: Any) -> None:
104
+ if self.timestamp is None:
105
+ self.timestamp = (ctx or {}).get("timestamp") or datetime.now()
106
+
107
+ @property
108
+ def matchid(self) -> str:
109
+ return self.metadata.sourceID
110
+
111
+ @computed_field # type: ignore[prop-decorator]
112
+ @property
113
+ def status(self) -> LiveStatus:
114
+ if self.timerInfo:
115
+ if self.timerInfo.type == "Warmup":
116
+ return LiveStatus.WARMUP
117
+
118
+ if self.timerInfo.type == "UntilStartOfFirstGame":
119
+ return LiveStatus.PREPARE
120
+
121
+ elif self.timerInfo.totalSeconds == 15:
122
+ return LiveStatus.FIFTEENSECONDS
123
+
124
+ return LiveStatus.BETWEENGAMES
125
+
126
+ elif self.score:
127
+ if self.isMatchBall:
128
+ return LiveStatus.MATCHBALL
129
+
130
+ elif self.isGameBall:
131
+ return LiveStatus.GAMEBALL
132
+
133
+ elif self.isVictoryFor:
134
+ return LiveStatus.FINISHED
135
+
136
+ return LiveStatus.ONGOING
137
+
138
+ return LiveStatus.READY
139
+
140
+ def nr_games_played(self) -> int:
141
+ return sum(int(x) for x in self.result.split("-"))
142
+
143
+ @computed_field # type: ignore[prop-decorator]
144
+ @property
145
+ def scores(self) -> ScoresType:
146
+ if self.gamescores is None:
147
+ return []
148
+
149
+ ret = []
150
+ for gscore in self.gamescores.split(","):
151
+ ret.append((int((s := gscore.split("-"))[0]), int(s[1])))
152
+ return ret
153
+
154
+ @field_serializer("scores", mode="plain")
155
+ def _scores_as_lists(self, value: ScoresType) -> list[list[int]]:
156
+ # Convert the tuples to lists for the schema, because otherwise json2ts
157
+ # loses the element typing and emits [unknown, unknown].
158
+ return [list(game) for game in value]
159
+
160
+ def _get_scoreline(
161
+ self, scores: list[str], offsets: list[int] | None
162
+ ) -> list[Point]:
163
+ ret: list[Point] = []
164
+
165
+ prev_offset = 0
166
+ for point, offset in itertools.zip_longest(scores, offsets or []):
167
+ ret.append(
168
+ make_point_from_squore_line(point, duration=offset - prev_offset)
169
+ )
170
+ prev_offset = offset
171
+
172
+ return ret
173
+
174
+ @computed_field # type: ignore[prop-decorator]
175
+ @property
176
+ def games(self) -> list[Game]:
177
+ nr_games_played = self.nr_games_played()
178
+ ret = []
179
+
180
+ is_cur = False
181
+ for i, ((a, b), points, timings) in enumerate(
182
+ zip(
183
+ self.scores,
184
+ self.score,
185
+ [t for t in self.timing if t.offsets is not None],
186
+ strict=True,
187
+ )
188
+ ):
189
+ scoreline = self._get_scoreline(points, timings.offsets)
190
+
191
+ is_cur = i == nr_games_played
192
+ ret.append(
193
+ Game(
194
+ score=(a, b),
195
+ starttime=datetime.fromisoformat(timings.start),
196
+ endtime=None if is_cur else datetime.fromisoformat(timings.end),
197
+ winner=None if is_cur else int(a < b),
198
+ scoreline=scoreline,
199
+ )
200
+ )
201
+
202
+ if (
203
+ not is_cur
204
+ and self.status >= LiveStatus.READY
205
+ and self.status < LiveStatus.FINISHED
206
+ ) or (
207
+ len(self.timing) == 1
208
+ and self.timing[0].offsets is None
209
+ and self.score == []
210
+ ):
211
+ ret.append(Game(score=(0, 0)))
212
+ return ret
213
+
214
+ @computed_field # type: ignore[prop-decorator]
215
+ @property
216
+ def winner(self) -> int | None:
217
+ if self.isVictoryFor is None:
218
+ return None
219
+ return {"A": 0, "B": 1}[self.isVictoryFor]
220
+
221
+ def validate_livedata(self) -> Never | None:
222
+ if len([t for t in self.timing if t.offsets is not None]) != len(self.scores):
223
+ raise InconsistentStateError(
224
+ matchid=self.matchid, deviceid=self.liveScoreDeviceId
225
+ )
226
+ return None
227
+
228
+ @field_validator("court", mode="before")
229
+ @classmethod
230
+ def handle_no_court_as_string(
231
+ cls, data: Literal["null"] | int | None
232
+ ) -> int | None:
233
+ return None if data == "null" else data
@@ -0,0 +1,63 @@
1
+ # needed < 3.14 so that annotations aren't evaluated
2
+ from __future__ import annotations
3
+
4
+ from enum import IntEnum, auto
5
+
6
+
7
+ class LiveStatus(IntEnum):
8
+ """The status of a live match, derived from the Squore live data."""
9
+
10
+ UNKNOWN = 0
11
+ WARMUP = auto()
12
+ PREPARE = auto()
13
+ FIFTEENSECONDS = auto()
14
+ READY = auto()
15
+ ONGOING = auto()
16
+ GAMEBALL = auto()
17
+ BETWEENGAMES = auto()
18
+ MATCHBALL = auto()
19
+ FINISHED = auto()
20
+ EXTERNAL = auto()
21
+
22
+ def __str__(self) -> str:
23
+ return self.name
24
+
25
+ @property
26
+ def short(self) -> str:
27
+ return {
28
+ self.UNKNOWN: "???",
29
+ self.WARMUP: "WUP",
30
+ self.PREPARE: "PRE",
31
+ self.READY: "RDY",
32
+ self.ONGOING: "ONG",
33
+ self.GAMEBALL: "GBL",
34
+ self.BETWEENGAMES: "PAU",
35
+ self.FIFTEENSECONDS: "15s",
36
+ self.MATCHBALL: "MBL",
37
+ self.FINISHED: "FIN",
38
+ self.EXTERNAL: "EXT",
39
+ }[self]
40
+
41
+ def can_come_after(self, other: LiveStatus) -> bool:
42
+ if self == self.UNKNOWN:
43
+ return False
44
+
45
+ elif other < self.ONGOING:
46
+ return True
47
+
48
+ elif self == self.ONGOING and other in (
49
+ self.BETWEENGAMES,
50
+ self.FIFTEENSECONDS,
51
+ self.GAMEBALL,
52
+ self.MATCHBALL,
53
+ ):
54
+ return True
55
+
56
+ elif self == self.FIFTEENSECONDS and other in (
57
+ self.GAMEBALL,
58
+ self.BETWEENGAMES,
59
+ self.PREPARE,
60
+ ):
61
+ return True
62
+
63
+ return self >= other
squoredata/match.py ADDED
@@ -0,0 +1,84 @@
1
+ """The static Squore match description, i.e. a match without live data."""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from .devinfo import SquoreDeviceInfo
8
+
9
+
10
+ class Players(BaseModel, extra="forbid"):
11
+ """The names of the two players."""
12
+
13
+ A: str
14
+ B: str
15
+
16
+
17
+ class PlayersProps(BaseModel, extra="forbid"):
18
+ """A per-player attribute, one value for each player."""
19
+
20
+ A: str | None = None
21
+ B: str | None = None
22
+
23
+
24
+ class Colors(PlayersProps): ...
25
+
26
+
27
+ class Countries(PlayersProps): ...
28
+
29
+
30
+ class Clubs(PlayersProps): ...
31
+
32
+
33
+ class Event(BaseModel, extra="forbid"):
34
+ """The event a match belongs to."""
35
+
36
+ name: str
37
+ division: str
38
+
39
+
40
+ class Format(BaseModel, extra="forbid"):
41
+ """The scoring format of a match."""
42
+
43
+ numberOfPointsToWinGame: int = 11
44
+ numberOfGamesToWinMatch: int = 3
45
+ useHandInHandOutScoring: bool = False
46
+
47
+
48
+ class Wifi(BaseModel, extra="forbid"):
49
+ """The WiFi details a device is connected to."""
50
+
51
+ ipaddress: str | None = None
52
+ ssid: str | None = None
53
+
54
+
55
+ class Metadata(BaseModel, extra="forbid"):
56
+ """Metadata a Squore tablet attaches to a match."""
57
+
58
+ sourceID: str
59
+ device: SquoreDeviceInfo | None = None
60
+ source: str | None = None
61
+ version: int | None = None
62
+ language: str | None = None
63
+ wifi: Wifi | None = None
64
+ shareURL: str | None = None
65
+ sourceFeedbackState: (
66
+ Literal["SourceAcceptedFinalResult"] | Literal["SourceRejectedResult"] | None
67
+ ) = None
68
+ sourcePostResultUrl: str | None = None
69
+
70
+
71
+ class SquoreMatch(BaseModel, extra="forbid"):
72
+ """A match as used by Squore, without any live information."""
73
+
74
+ players: Players
75
+ clubs: Clubs | None = None
76
+ colors: Colors | None = None
77
+ countries: Countries | None = None
78
+ event: Event
79
+ format: Format
80
+ metadata: Metadata
81
+
82
+ @property
83
+ def matchid(self) -> str:
84
+ return self.metadata.sourceID
squoredata/point.py ADDED
@@ -0,0 +1,109 @@
1
+ """Point-level data as used by Squore.
2
+
3
+ The types and the parser are ported from the ``tcboard`` project so that the
4
+ Squore data structures are self-contained.
5
+ """
6
+
7
+ import re
8
+ from typing import Literal
9
+
10
+ from pydantic import BaseModel, TypeAdapter, ValidationError
11
+
12
+ __all__ = [
13
+ "AppealType",
14
+ "PlayerIndex",
15
+ "PlayerLetter",
16
+ "Point",
17
+ "ServerSide",
18
+ "make_point_from_squore_line",
19
+ ]
20
+
21
+ type PlayerLetter = Literal["A"] | Literal["B"]
22
+ type PlayerIndex = Literal[0] | Literal[1]
23
+ type ServerSide = Literal["L"] | Literal["R"]
24
+ type AppealType = Literal["S"] | Literal["L"] | Literal["N"]
25
+
26
+
27
+ class Point(BaseModel):
28
+ """A single point in a game."""
29
+
30
+ server: PlayerIndex | None
31
+ serveside: ServerSide | None
32
+ player: PlayerIndex
33
+ data: int | AppealType
34
+ duration: int | None = None
35
+
36
+
37
+ PATTERN = re.compile(
38
+ r"(?P<AX>(?P<AS>[-RL])(?P<AP>\d+|-)|ST|[YN]L)"
39
+ r"(?P<BX>(?P<BS>[-RL])(?P<BP>\d+|-)|ST|[YN]L)",
40
+ )
41
+
42
+ type PointDataType = AppealType | int
43
+ PointDataTypeValidator: TypeAdapter[PointDataType] = TypeAdapter(PointDataType)
44
+ PlayerIndexValidator: TypeAdapter[PlayerIndex] = TypeAdapter(PlayerIndex)
45
+ PlayerLetterValidator: TypeAdapter[PlayerLetter] = TypeAdapter(PlayerLetter)
46
+ ServerSideValidator: TypeAdapter[ServerSide] = TypeAdapter(ServerSide)
47
+
48
+
49
+ def make_point_from_squore_line(
50
+ squore: str | None, *, duration: int | None = None
51
+ ) -> Point:
52
+ """Parse a Squore line representation into a :class:`Point`.
53
+
54
+ Args:
55
+ squore: The Squore line representation, e.g. ``"R1--"``.
56
+ duration: The optional duration of the point in seconds.
57
+
58
+ Returns:
59
+ The parsed point.
60
+
61
+ Raises:
62
+ ValueError: If the line cannot be parsed.
63
+ """
64
+ m = None
65
+ if squore is not None:
66
+ m = re.fullmatch(PATTERN, squore)
67
+
68
+ if m is None:
69
+ raise ValueError(f"Not a Squore line I can parse: {squore}")
70
+
71
+ d = m.groupdict()
72
+
73
+ winner = int((a := d["AX"]) == "--") # bool to int: True→1, False→0
74
+ server, side = None, None
75
+ data: PointDataType
76
+ match a, d["BX"]:
77
+ case ("ST", _) | (_, "ST"):
78
+ data = "S"
79
+ case ("YL", _) | (_, "YL"):
80
+ data = "L"
81
+ case ("NL", _) | (_, "NL"):
82
+ data = "N"
83
+ case _:
84
+ try:
85
+ winner = PlayerIndexValidator.validate_python(
86
+ # bool to int: True→1, False→0
87
+ int(d["AP"] == "-")
88
+ )
89
+ server = PlayerIndexValidator.validate_python(
90
+ # bool to int: True→1, False→0
91
+ int(d["AS"] == "-")
92
+ )
93
+ side = ServerSideValidator.validate_python(
94
+ a if (a := d["AS"]) != "-" else d["BS"]
95
+ )
96
+ data = PointDataTypeValidator.validate_python(
97
+ a if (a := d["AP"]) != "-" else d["BP"]
98
+ )
99
+
100
+ except ValidationError as err:
101
+ raise ValueError(f"Not a Squore line I can parse: {squore}") from err
102
+
103
+ return Point(
104
+ server=server,
105
+ serveside=side,
106
+ data=data,
107
+ player=1 if winner else 0,
108
+ duration=duration,
109
+ )
squoredata/py.typed ADDED
File without changes
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.5
2
+ Name: squoredata
3
+ Version: 0.1.0
4
+ Summary: Self-contained Pydantic data structures used by Squore tablets
5
+ Author-email: "martin f. krafft" <squoredata@pobox.madduck.net>
6
+ License-File: LICENSE
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.13
11
+ Requires-Dist: pydantic
12
+ Provides-Extra: dev
13
+ Requires-Dist: coverage; extra == 'dev'
14
+ Requires-Dist: mypy; extra == 'dev'
15
+ Requires-Dist: pytest; extra == 'dev'
16
+ Requires-Dist: pytest-asyncio; extra == 'dev'
17
+ Requires-Dist: pytest-cov; extra == 'dev'
18
+ Requires-Dist: pytest-mock; extra == 'dev'
19
+ Requires-Dist: pytest-ruff; extra == 'dev'
20
+ Requires-Dist: ruff; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # squoredata
24
+
25
+ Self-contained [Pydantic](https://docs.pydantic.dev) v2 data structures used by
26
+ [Squore](https://github.com/obbimi/Squore) scoreboard tablets.
27
+
28
+ The package was extracted from [`tcboard`](https://github.com/madduck/tcboard).
29
+ It contains only the data models, not the surrounding server logic. TypeScript
30
+ bindings are generated from the models so that a JavaScript or TypeScript
31
+ project can consume the same shape.
32
+
33
+ ## Models
34
+
35
+ - `SquoreMatch` — a match as used by Squore, without any live information.
36
+ - `SquoreMatchLiveData` — a `SquoreMatch` together with the live data a tablet
37
+ reports. It adds the `court` and `timestamp` fields.
38
+
39
+ Supporting models: `Players`, `Colors`, `Countries`, `Clubs`, `Event`, `Format`,
40
+ `Wifi`, `Metadata`, `SquoreDeviceInfo`, `TimerInfo`, `Timing`, `When`, `Point`,
41
+ `Game`, and the `LiveStatus` enum.
42
+
43
+ ## Install
44
+
45
+ ```sh
46
+ pip install squoredata
47
+ ```
48
+
49
+ ## TypeScript bindings
50
+
51
+ `scripts/generate_ts.py` renders the JSON Schema of each model and converts it
52
+ to TypeScript with
53
+ [`json-schema-to-typescript`](https://www.npmjs.com/package/json-schema-to-typescript).
54
+ The output lives in `typescript/` and is committed:
55
+
56
+ - `SquoreMatch.d.ts`, `SquoreMatchLiveData.d.ts` — the interfaces
57
+ - `SquoreMatch.schema.json`, `SquoreMatchLiveData.schema.json` — the schemata
58
+ - `index.d.ts` — re-exports
59
+
60
+ The Hatch build hook (`hatch_build.py`) runs the generator before a wheel is
61
+ built, so a release can never carry stale bindings.
62
+
63
+ Install the generated bindings into another project from the `typescript/`
64
+ directory, or from a published `squoredata-types` package:
65
+
66
+ ```sh
67
+ npm install /path/to/squoredata/typescript
68
+ ```
69
+
70
+ ## Golden tests
71
+
72
+ The files in `golden/` are the shared contract between the two sides:
73
+
74
+ - `tests/test_golden.py` serialises and re-validates the models against the
75
+ golden JSON.
76
+ - `typescript/tests/golden.ts` assigns the same JSON to the generated
77
+ interfaces, so `tsc --noEmit` fails when the two sides diverge.
78
+
79
+ Regenerate the golden files with `python scripts/make_golden.py` after a model
80
+ change.
81
+
82
+ ## Development
83
+
84
+ ```sh
85
+ pip install -e .[dev]
86
+ ruff check .
87
+ pytest
88
+ mypy --strict .
89
+
90
+ npm install
91
+ node_modules/.bin/tsc --noEmit -p tsconfig.json
92
+ ```
@@ -0,0 +1,12 @@
1
+ squoredata/__init__.py,sha256=V2-mhv1O2mLD4AD74TF4BAIHfRqWxLdKPaTDDXMSbQ4,1359
2
+ squoredata/devinfo.py,sha256=N5KfBVzQvdWVKo90ulCkSsZHx4F0oU8JoXivv7S1X_k,1686
3
+ squoredata/game.py,sha256=cou5uHA5Tje2g2Ppu9fHhCP9pIaYpFRbemDmNdFHTgY,1146
4
+ squoredata/livedata.py,sha256=R6Nj4Bs9T14f4om4XHDU48MhCQRbKTQx8kxEqTnN_1Q,6933
5
+ squoredata/livestatus.py,sha256=8bWQoZBBftSPuDyb0fDlBaXVQkysvliKfvwnkA7Y12k,1558
6
+ squoredata/match.py,sha256=OLCQZ52WNoOOIGmharNOnbkyyLXLcKDHwh1dwR7VgHE,1887
7
+ squoredata/point.py,sha256=-s9EwOHy_G9X91pV-D4yY6SYkPvdLkFW2VQ-NKReU1A,3171
8
+ squoredata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ squoredata-0.1.0.dist-info/METADATA,sha256=lAgmvWSLTU4aPXS1tEy5ZzsTLwfpKn8h-J2eGJSnFjE,2997
10
+ squoredata-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
11
+ squoredata-0.1.0.dist-info/licenses/LICENSE,sha256=7fO2Xb6dFmiG-ouqDpqSjikytIJF-y2An2ga8jVGLb4,1073
12
+ squoredata-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 martin f. krafft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.