PFCSchemas 0.1.1__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.
pfcschemas/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from pfcschemas!"
pfcschemas/ajson.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+ from pydantic import BaseModel
3
+ from pfcschemas import fcj
4
+ import pandas as pd
5
+ from datetime import datetime
6
+ from pfcschemas.utils.files import validate_json
7
+ from pfcschemas.ma import MA
8
+ from pfcschemas.sinfo import ScheduleInfo
9
+
10
+
11
+ class AJson(BaseModel):
12
+ origin: fcj.Origin | None = None
13
+ isComp: bool
14
+ sourceBin: str | None = None
15
+ sourceFCJ: str | None = None
16
+ bootTime: datetime | None = None
17
+ mans: list[MA]
18
+
19
+ def schedule(self):
20
+ schedules = [man.schedule for man in self.mans]
21
+ if all([s == schedules[0] for s in schedules[1:]]):
22
+ return schedules[0].fcj_to_pfc()
23
+ else:
24
+ return ScheduleInfo.mixed()
25
+
26
+ def all_versions(self):
27
+ versions = set()
28
+ for man in self.mans:
29
+ versions |= set(man.history.keys())
30
+ return list(versions)
31
+
32
+ def get_scores(self, version: str, props: fcj.ScoreProperties=None, group="total"):
33
+ if props is None:
34
+ props = fcj.ScoreProperties()
35
+ scores = {}
36
+ for man in self.mans:
37
+ if version in man.history:
38
+ scores[man.name] = man.history[version].get_score(props).__dict__[group]
39
+ return pd.Series(scores, name=version)
40
+
41
+ def create_score_df(
42
+ self, props: fcj.ScoreProperties, group="total", version: str = "All"
43
+ ):
44
+ versions = self.all_versions() if version == "All" else [version]
45
+ return pd.concat([self.get_scores(ver, props, group) for ver in versions], axis=1)
46
+
47
+ def check_version(self, version: str):
48
+ version = version[1:] if version.startswith('v') else version
49
+ return all([man.history is not None and version in man.history.keys() for man in self.mans])
50
+
51
+ @staticmethod
52
+ def parse_json(json: dict|str):
53
+ return AJson.model_validate(validate_json(json))
pfcschemas/fcj.py ADDED
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import re
5
+
6
+ import pandas as pd
7
+ from pydantic import BaseModel
8
+ from typing import Annotated
9
+
10
+
11
+ class FCJ(BaseModel):
12
+ version: str
13
+ comments: str
14
+ name: str
15
+ view: View
16
+ parameters: Parameters
17
+ scored: bool
18
+ scores: list[float]
19
+ human_scores: list[HumanResult] = []
20
+ fcs_scores: list[Result] = []
21
+ mans: list[Man]
22
+ data: list[Data]
23
+ jhash: int | None = None
24
+
25
+ def score_df(self):
26
+ return pd.concat(
27
+ {fcjr.fa_version: fcjr.to_df() for fcjr in self.fcs_scores},
28
+ axis=0,
29
+ names=["version", "manoeuvre", "difficulty", "truncate"],
30
+ )
31
+
32
+ def man_df(self):
33
+ return pd.DataFrame(
34
+ [man.__dict__ for man in self.mans[1:-1]],
35
+ index=pd.Index(range(len(self.mans[1:-1])), name="manoeuvre"),
36
+ )
37
+
38
+ def pfc_version_df(self):
39
+ sdf = self.score_df().loc[pd.IndexSlice[:, :, 3, False]]
40
+ return pd.concat(
41
+ [sdf, sdf.mul(self.man_df().k, axis=0)], axis=1, keys=["raw", "kfac"]
42
+ )
43
+
44
+ def version_summary_df(self):
45
+ return self.pfc_version_df().groupby("version").kfac.sum()
46
+
47
+ def latest_version(self):
48
+ return max([fcjr.fa_version for fcjr in self.fcs_scores])
49
+
50
+ @property
51
+ def id(self):
52
+ return re.search(r"\d{8}", self.name)[0]
53
+
54
+ @property
55
+ def created(self):
56
+ try:
57
+ return datetime.datetime.strptime(
58
+ re.search(r"_\d{2}_\d{2}_\d{2}_", self.name)[0], "_%y_%m_%d_"
59
+ )
60
+ except Exception:
61
+ return None
62
+
63
+
64
+ class View(BaseModel):
65
+ position: dict
66
+ target: dict
67
+
68
+
69
+ class Parameters(BaseModel):
70
+ rotation: float
71
+ start: int
72
+ stop: int
73
+ moveEast: float
74
+ moveNorth: float
75
+ wingspan: float
76
+ modelwingspan: float
77
+ elevate: float
78
+ originLat: float
79
+ originLng: float
80
+ originAlt: float
81
+ pilotLat: str | float
82
+ pilotLng: str | float
83
+ pilotAlt: str | float
84
+ centerLat: str | float
85
+ centerLng: str | float
86
+ centerAlt: str | float
87
+ schedule: list[str]
88
+
89
+
90
+ class HumanResult(BaseModel):
91
+ name: str
92
+ date: datetime.date
93
+ scores: list[float]
94
+
95
+
96
+ class Result(BaseModel):
97
+ fa_version: str
98
+ manresults: list[ManResult | None]
99
+
100
+ def to_df(self) -> pd.DataFrame:
101
+ return pd.concat(
102
+ {i: fcjmr.to_df() for i, fcjmr in enumerate(self.manresults[1:]) if fcjmr},
103
+ axis=0,
104
+ names=["manoeuvre", "difficulty", "truncate"],
105
+ )
106
+
107
+
108
+ class ManResult(BaseModel):
109
+ els: list[El]
110
+ results: list[Score] = []
111
+
112
+ def to_df(self) -> pd.DataFrame:
113
+ return pd.DataFrame(
114
+ data=[res.score.__dict__ for res in self.results],
115
+ index=pd.MultiIndex.from_frame(
116
+ pd.DataFrame([res.properties.__dict__ for res in self.results])
117
+ ),
118
+ )
119
+
120
+ def get_score(self, props: ScoreProperties):
121
+ for r in self.results:
122
+ if r.properties == props:
123
+ return r.score
124
+
125
+
126
+ class El(BaseModel):
127
+ name: str
128
+ start: int
129
+ stop: int
130
+
131
+
132
+ class Score(BaseModel):
133
+ score: ScoreValues
134
+ properties: ScoreProperties
135
+
136
+
137
+ class ScoreValues(BaseModel):
138
+ intra: float
139
+ inter: float
140
+ positioning: float
141
+ total: float
142
+
143
+
144
+ class ScoreProperties(BaseModel):
145
+ difficulty: int = 3
146
+ truncate: bool = False
147
+
148
+ def __eq__(self, other):
149
+ if not isinstance(other, ScoreProperties):
150
+ return False
151
+ return self.difficulty == other.difficulty and self.truncate == other.truncate
152
+
153
+
154
+ class Man(BaseModel):
155
+ name: str
156
+ k: float
157
+ id: str
158
+ sp: int
159
+ wd: float
160
+ start: int
161
+ stop: int
162
+ sel: bool
163
+ background: str
164
+
165
+
166
+ class Data(BaseModel):
167
+ VN: float = None
168
+ VE: float = None
169
+ VD: float = None
170
+ dPD: float = None #
171
+ r: float
172
+ p: float
173
+ yw: float
174
+ N: float
175
+ E: float
176
+ D: float
177
+ time: int
178
+ roll: float
179
+ pitch: float
180
+ yaw: float
181
+
182
+
183
+ def get_scores(file: str) -> pd.DataFrame:
184
+ fcj = FCJ.model_validate_json(open(file, "r").read())
185
+ return fcj.pfc_version_df()
186
+
187
+
188
+ class Origin(BaseModel):
189
+ lat: Annotated[float, "latitude in degrees"]
190
+ lng: Annotated[float, "latitude in degrees"]
191
+ alt: Annotated[float, "height AMSL in meters"]
192
+ heading: Annotated[float, "heading (direction of box Y axis) in degrees from North"]
193
+ move_north: Annotated[float, "non-standard offset of box"]=0
194
+ move_east: Annotated[float, "non-standard offset of box"]=0
195
+ move_down: Annotated[float, "non-standard offset of box"]=0
196
+
pfcschemas/ma.py ADDED
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ import pandas as pd
4
+ from pydantic import BaseModel
5
+
6
+ from pfcschemas import fcj
7
+ from pfcschemas.sinfo import ScheduleInfo
8
+ from pfcschemas.maninfo import ManInfo
9
+
10
+ class MDef(BaseModel):
11
+ info: ManInfo
12
+ mps: dict
13
+ eds: dict
14
+ box: dict
15
+
16
+
17
+ class MA(BaseModel):
18
+ name: str
19
+ id: int
20
+ schedule: ScheduleInfo
21
+ schedule_direction: str | None
22
+ flown: list[dict] | dict
23
+
24
+ history: dict[str, fcj.ManResult] | None = None
25
+
26
+ mdef: MDef | None = None
27
+ manoeuvre: dict | None = None
28
+ template: list[dict] | dict | None = None
29
+ corrected: dict | None = None
30
+ corrected_template: list[dict] | dict | None = None
31
+ scores: dict | None = None
32
+
33
+ def basic(self):
34
+ return MA(
35
+ name=self.name,
36
+ id=self.id,
37
+ schedule=self.schedule,
38
+ schedule_direction=self.schedule_direction,
39
+ flown=self.flown,
40
+ history=self.history,
41
+ )
42
+
43
+ def add_mdef(self, mdef: MDef) -> MA:
44
+ return MA(**(self.__dict__ | dict(mdef=mdef)))
45
+
46
+ def simplify_history(self):
47
+ vnames = [v[1:] if v.startswith("v") else v for v in self.history.keys()]
48
+ vnames_old = vnames[::-1]
49
+ vnids = [
50
+ len(vnames) - vnames_old.index(vn) - 1
51
+ for vn in list(pd.Series(vnames).unique())
52
+ ]
53
+
54
+ return MA(
55
+ **(
56
+ self.__dict__
57
+ | dict(
58
+ history={vnames[i]: list(self.history.values())[i] for i in vnids}
59
+ )
60
+ )
61
+ )
62
+
63
+
64
+ # vids = [vnames.rindex(vn) for vn in set(vnames)]
pfcschemas/maninfo.py ADDED
@@ -0,0 +1,53 @@
1
+ from .positioning import Position, BoxLocation
2
+ from typing import Tuple, Annotated
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ManInfo(BaseModel):
7
+ name: str
8
+ short_name: str
9
+ k: float
10
+ position: Position
11
+ start: BoxLocation
12
+ end: BoxLocation
13
+ centre_points: Annotated[
14
+ list[int],
15
+ "points that should be centered, ids correspond to the previous element",
16
+ ] = []
17
+ centred_els: Annotated[
18
+ list[Tuple[int, float]], "element ids that should be centered"
19
+ ] = []
20
+
21
+ def to_dict(self):
22
+ return self.model_dump()
23
+
24
+ @staticmethod
25
+ def from_dict(data: dict):
26
+ return ManInfo.model_validate(data)
27
+
28
+
29
+ def maninfomaker(
30
+ name: str,
31
+ short_name: str,
32
+ k: float,
33
+ position: Position,
34
+ start: BoxLocation,
35
+ end: BoxLocation,
36
+ centre_points: Annotated[
37
+ list[int],
38
+ "points that should be centered, ids correspond to the previous element",
39
+ ] = None,
40
+ centred_els: Annotated[
41
+ list[Tuple[int, float]], "element ids that should be centered"
42
+ ] = None,
43
+ ):
44
+ return ManInfo(
45
+ name=name,
46
+ short_name=short_name,
47
+ k=k,
48
+ position=position,
49
+ start=start,
50
+ end=end,
51
+ centre_points=centre_points if centre_points is not None else [],
52
+ centred_els=centred_els if centred_els is not None else [],
53
+ )
@@ -0,0 +1,100 @@
1
+ import numpy as np
2
+ from pydantic import BaseModel
3
+ from typing import Annotated
4
+ from .utils.enum import EnumStr
5
+
6
+
7
+ class Orientation(EnumStr):
8
+ UPRIGHT = np.pi
9
+ INVERTED = 0
10
+
11
+
12
+ class Heading(EnumStr):
13
+ RTOL = np.pi
14
+ LTOR = 0
15
+ OUTTOIN = 3 * np.pi / 2
16
+ INTOOUT = np.pi / 2
17
+
18
+ @staticmethod
19
+ def values():
20
+ return np.array(list(Heading.__members__.values()))
21
+
22
+ @staticmethod
23
+ def infer(bearing: Annotated[float, "in radians from north"]):
24
+ def check(bearing: float, heading: Heading):
25
+ return (
26
+ np.round(np.abs(4 * (bearing - heading.value)) / (2 * np.pi)).astype(
27
+ int
28
+ )
29
+ % 4
30
+ ) == 0
31
+
32
+ for head in Heading.__members__.values():
33
+ if check(bearing, head):
34
+ return head
35
+ else:
36
+ raise ValueError(f"Invalid bearing {bearing}")
37
+
38
+ def reverse(self):
39
+ return {
40
+ Heading.RTOL: Heading.LTOR,
41
+ Heading.LTOR: Heading.RTOL,
42
+ Heading.OUTTOIN: Heading.INTOOUT,
43
+ Heading.INTOOUT: Heading.OUTTOIN,
44
+ }[self]
45
+
46
+
47
+ class Direction(EnumStr):
48
+ UPWIND = 1
49
+ DOWNWIND = -1
50
+ CROSS = 0
51
+
52
+ def wind_swap_heading(self, d_or_w: Heading) -> int:
53
+ match self:
54
+ case Direction.UPWIND:
55
+ return d_or_w
56
+ case Direction.DOWNWIND:
57
+ return d_or_w.reverse()
58
+ case Direction.CROSS:
59
+ return d_or_w
60
+
61
+ @staticmethod
62
+ def parse(s: str):
63
+ match s[0].lower():
64
+ case "u":
65
+ return Direction.UPWIND
66
+ case "d":
67
+ return Direction.DOWNWIND
68
+ case "c":
69
+ return Direction.CROSS
70
+ case _:
71
+ raise ValueError(f"Invalid wind {s}")
72
+
73
+
74
+ class Height(EnumStr):
75
+ BTM = 0.2
76
+ MID = 0.6
77
+ TOP = 1.0
78
+
79
+
80
+ class Position(EnumStr):
81
+ CENTRE = 0
82
+ END = 1
83
+
84
+
85
+ class BoxLocation(BaseModel):
86
+ height: Height
87
+ direction: Direction | None = None
88
+ orientation: Orientation | None = None
89
+
90
+
91
+ def boxlocationmaker(
92
+ height: Height,
93
+ direction: Direction | None = None,
94
+ orientation: Orientation | None = None,
95
+ ):
96
+ return BoxLocation(
97
+ height=height,
98
+ direction=direction,
99
+ orientation=orientation,
100
+ )
pfcschemas/py.typed ADDED
File without changes
pfcschemas/sdef.py ADDED
@@ -0,0 +1,31 @@
1
+ from flightanalysis import SchedDef
2
+ from pydantic import BaseModel
3
+
4
+ from pfcschemas.sinfo import ScheduleInfo
5
+
6
+
7
+ class DirectionDefinition(BaseModel):
8
+ manid: int
9
+ direction: str
10
+
11
+ @staticmethod
12
+ def from_sdef(sdef: SchedDef):
13
+ manid = sdef.wind_def_manoeuvre()
14
+ return DirectionDefinition(
15
+ manid=manid, direction=sdef[manid].info.start.direction.name
16
+ )
17
+
18
+
19
+ class SDefFile(BaseModel):
20
+ category: str
21
+ schedule: str
22
+ direction_definition: DirectionDefinition
23
+ fa_version: str
24
+ mdefs: dict[str, dict | list[dict]]
25
+
26
+ @property
27
+ def sinfo(self):
28
+ return ScheduleInfo(self.category, self.schedule)
29
+
30
+ def create_definition(self):
31
+ return SchedDef.from_dict(self.mdefs)
pfcschemas/sinfo.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Literal
4
+
5
+
6
+ fcj_categories = {
7
+ "F3A FAI": "f3a",
8
+ "F3A": "f3a",
9
+ "US AMA": "nsrca",
10
+ "F3A UK": "f3auk",
11
+ "F3A US": "nsrca",
12
+ "IMAC": "imac",
13
+ }
14
+
15
+ fcj_schedules = {
16
+ "P23": "p23",
17
+ "F23": "f23",
18
+ "P25": "p25",
19
+ "F25": "f25",
20
+ "Unlimited 2024": "unlimited2024",
21
+ }
22
+
23
+
24
+ def lookup(val, data):
25
+ val = val.replace("_", " ")
26
+ return data[val] if val in data else val
27
+
28
+
29
+ @dataclass
30
+ class ManDetails:
31
+ name: str
32
+ id: int
33
+ k: float
34
+ entry: Literal["UPWIND", "DOWNWIND", "CROSS"]
35
+
36
+
37
+ @dataclass
38
+ class ScheduleInfo:
39
+ category: str
40
+ name: str
41
+
42
+ @staticmethod
43
+ def from_str(fname):
44
+ if fname.endswith(".sdef"):
45
+ fname = fname[:-6]
46
+ info = fname.split("_")
47
+ if len(info) == 1:
48
+ return ScheduleInfo("f3a", info[0].lower())
49
+ else:
50
+ return ScheduleInfo(info[0].lower(), info[1].lower())
51
+
52
+ def __str__(self):
53
+ return f"{self.category}_{self.name}".lower()
54
+
55
+ @staticmethod
56
+ def lookupCategory(category):
57
+ return lookup(category, fcj_categories)
58
+
59
+ @staticmethod
60
+ def lookupSchedule(schedule):
61
+ return lookup(schedule, fcj_schedules)
62
+
63
+ @staticmethod
64
+ def mixed():
65
+ return ScheduleInfo("na", "mixed")
66
+
67
+ def fcj_to_pfc(self):
68
+ return ScheduleInfo(
69
+ lookup(self.category, fcj_categories), lookup(self.name, fcj_schedules)
70
+ )
71
+
72
+ def pfc_to_fcj(self):
73
+ def rev_lookup(val, data):
74
+ return (
75
+ next(k for k, v in data.items() if v == val)
76
+ if val in data.values()
77
+ else val
78
+ )
79
+
80
+ return ScheduleInfo(
81
+ rev_lookup(self.category, fcj_categories),
82
+ rev_lookup(self.name, fcj_schedules),
83
+ )
84
+
85
+ @staticmethod
86
+ def from_fcj_sch(sch):
87
+ return ScheduleInfo(*sch).fcj_to_pfc()
88
+
89
+ def to_fcj_sch(self):
90
+ return list(self.pfc_to_fcj().__dict__.values())
91
+
92
+ @staticmethod
93
+ def build(category, name):
94
+ return ScheduleInfo(category.lower(), name.lower())
95
+
96
+ def manoeuvre_details(self) -> list[ManDetails]:
97
+ mds = []
98
+
99
+ for i, (k, v) in enumerate(self.json_data().items()):
100
+ if isinstance(v, list):
101
+ v = v[0]
102
+ mds.append(
103
+ ManDetails(
104
+ v["info"]["short_name"],
105
+ i + 1,
106
+ v["info"]["k"],
107
+ v["info"]["start"]["direction"],
108
+ )
109
+ )
110
+ return mds
111
+
112
+ def __eq__(self, other: ScheduleInfo):
113
+ return str(self.fcj_to_pfc()) == str(other.fcj_to_pfc())
File without changes
@@ -0,0 +1,31 @@
1
+ from enum import Enum
2
+ from pydantic_core import CoreSchema, core_schema
3
+ from pydantic import GetCoreSchemaHandler
4
+ from typing import Any
5
+
6
+ ##https://github.com/pydantic/pydantic/discussions/6466#discussioncomment-8219585
7
+ class EnumStr(float, Enum):
8
+ @classmethod
9
+ def __get_pydantic_core_schema__(
10
+ cls, _source_type: Any, _handler: GetCoreSchemaHandler
11
+ ) -> CoreSchema:
12
+ return core_schema.no_info_after_validator_function(
13
+ cls.validate,
14
+ core_schema.any_schema(),
15
+ serialization=core_schema.plain_serializer_function_ser_schema(
16
+ lambda x: x.name
17
+ ),
18
+ )
19
+
20
+ @classmethod
21
+ def validate(cls, v: Any):
22
+ if isinstance(v, cls):
23
+ return v
24
+ elif isinstance(v, str):
25
+ try:
26
+ return cls[v]
27
+ except KeyError:
28
+ return None
29
+ # raise ValueError(f"Invalid {cls.__name__}: {v}")
30
+ else:
31
+ raise ValueError(f"Unexpected type: {type(v)}")
@@ -0,0 +1,13 @@
1
+ import os
2
+ from json import load
3
+
4
+
5
+ def validate_json(file: dict|str|os.PathLike) -> dict:
6
+ if isinstance(file, dict):
7
+ return file
8
+ elif isinstance(file, str) or isinstance(file, os.PathLike):
9
+ with open(file, 'r') as f:
10
+ return load(f)
11
+ else:
12
+ raise ValueError("expected a dict, str or os.PathLike")
13
+
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.3
2
+ Name: PFCSchemas
3
+ Version: 0.1.1
4
+ Summary: Pydantic schemas for the pyflightcoach libraries
5
+ Author-email: Thomas David <thomasdavid0@gmail.com>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: numpy>=2.1.3
8
+ Requires-Dist: pandas>=2.2.3
9
+ Requires-Dist: pydantic>=2.9.2
10
+ Description-Content-Type: text/markdown
11
+
12
+ # PFCSchemas
13
+
14
+ Pydantic schemas for the pyflightcoach libraries
@@ -0,0 +1,15 @@
1
+ pfcschemas/__init__.py,sha256=5_H9g39iYb1gwU5zqH24Vysl7xRPESgeHYm6MvPGRUA,56
2
+ pfcschemas/ajson.py,sha256=DYL8wJPOXcV23tuFcq4VeRWGz8RYwdbpBCGIzYZLP3o,1852
3
+ pfcschemas/fcj.py,sha256=bh9MQ4SUm-HCiS6pYMwl30TJbucqhc0Cohb0o2x3xS0,4553
4
+ pfcschemas/ma.py,sha256=Qqjdvd_rwiNxJiG-HgmGfmwkpyt_8plO0GXg0EnMtRE,1623
5
+ pfcschemas/maninfo.py,sha256=eAsL4udEqXTyXt4QyqGOH64Sfes2LKEGXeUdo78wrlA,1360
6
+ pfcschemas/positioning.py,sha256=2sdZGCh91fqN8jvxsplz4LQFcJVzQVWnSINVNgQpk70,2318
7
+ pfcschemas/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ pfcschemas/sdef.py,sha256=ebiBoo5ZGzYIionyl4qAP0O0j9pemE9bsySfg1ASJFY,744
9
+ pfcschemas/sinfo.py,sha256=9B4zRasA3w_7k1q_GDmZIhk9qp_uzm1MsKVzpegNsWo,2704
10
+ pfcschemas/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pfcschemas/utils/enum.py,sha256=xmtyjq6uizAJfDIyyzg9iAGQ9cIjm8DsB04OYmZGQtU,1023
12
+ pfcschemas/utils/files.py,sha256=GBGcptZ3obpY8yvlzEOv7p5H3y9en58zGfTQWLn5Ans,346
13
+ pfcschemas-0.1.1.dist-info/METADATA,sha256=vGWeQ59lehH2FcUnN4Ij0RpkCGdTAwqu1oXTb_W4Pic,379
14
+ pfcschemas-0.1.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
15
+ pfcschemas-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any