atlas-schema 0.2.4__py3-none-any.whl → 0.4.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.
atlas_schema/_version.py CHANGED
@@ -1,16 +1,34 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
3
13
  TYPE_CHECKING = False
4
14
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
15
+ from typing import Tuple
16
+ from typing import Union
17
+
6
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
7
20
  else:
8
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
9
23
 
10
24
  version: str
11
25
  __version__: str
12
26
  __version_tuple__: VERSION_TUPLE
13
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.4.0'
32
+ __version_tuple__ = version_tuple = (0, 4, 0)
14
33
 
15
- __version__ = version = '0.2.4'
16
- __version_tuple__ = version_tuple = (0, 2, 4)
34
+ __commit_id__ = commit_id = None
atlas_schema/enums.py CHANGED
@@ -18,7 +18,7 @@ class MultipleEnumAccessMeta(EnumType):
18
18
  Enum Metaclass to provide a way to access multiple values all at once.
19
19
  """
20
20
 
21
- def __getitem__(self: type[_E], key: str | tuple[str]) -> _E | list[_E]: # type:ignore[misc,override]
21
+ def __getitem__(cls: type[_E], key: str | tuple[str]) -> _E | list[_E]: # type:ignore[misc,override]
22
22
  getitem = cast(Callable[[str], _E], super().__getitem__) # type:ignore[misc]
23
23
  if isinstance(key, tuple):
24
24
  return [getitem(name) for name in key]
atlas_schema/methods.py CHANGED
@@ -8,7 +8,6 @@ from operator import ior
8
8
  import awkward
9
9
  import particle
10
10
  from coffea.nanoevents.methods import base, candidate, vector
11
- from dask_awkward import dask_method
12
11
 
13
12
  from atlas_schema.enums import PhotonID
14
13
  from atlas_schema.typing_compat import Behavior
@@ -19,22 +18,127 @@ behavior.update(base.behavior)
19
18
  behavior.update(candidate.behavior)
20
19
 
21
20
 
21
+ def _set_repr_name(classname):
22
+ def namefcn(_self):
23
+ return classname
24
+
25
+ behavior[("__typestr__", classname)] = classname[0].lower() + classname[1:]
26
+ behavior[classname].__repr__ = namefcn
27
+
28
+
22
29
  class NtupleEvents(behavior["NanoEvents"]): # type: ignore[misc, valid-type, name-defined]
30
+ """Individual systematic variation of events."""
31
+
23
32
  def __repr__(self):
24
- return f"<event {getattr(self, 'runNumber', '??')}:\
25
- {getattr(self, 'eventNumber', '??')}:\
26
- {getattr(self, 'mcChannelNumber', '??')}>"
33
+ return f"<event {getattr(self, 'runNumber', '??')}:{getattr(self, 'eventNumber', '??')}:{getattr(self, 'mcChannelNumber', '??')}>"
27
34
 
35
+ def __getitem__(self, key):
36
+ """Support accessing systematic variations via bracket notation.
28
37
 
29
- behavior["NanoEvents"] = NtupleEvents
38
+ Args:
39
+ key: The systematic variation name. "NOSYS" returns the nominal events.
30
40
 
41
+ Returns:
42
+ The requested systematic variation or nominal events for "NOSYS".
43
+ """
44
+ if key == "NOSYS":
45
+ return self
46
+ return super().__getitem__(key)
31
47
 
32
- def _set_repr_name(classname):
33
- def namefcn(_self):
34
- return classname
48
+ @property
49
+ def systematic(self):
50
+ """Get the systematic variation name for this event collection."""
51
+ return "nominal"
35
52
 
36
- behavior[("__typestr__", classname)] = classname[0].lower() + classname[1:]
37
- behavior[classname].__repr__ = namefcn
53
+ @property
54
+ def systematic_names(self):
55
+ """Get all systematic variations available in this event collection.
56
+
57
+ Returns a list of systematic variation names, including 'NOSYS' for nominal.
58
+ """
59
+ # Get systematics from metadata stored during schema building
60
+ systematics = self.metadata.get("systematics", [])
61
+ return ["NOSYS", *systematics]
62
+
63
+ @property
64
+ def systematics(self):
65
+ """Get all systematic variations available in this event collection.
66
+
67
+ Returns a list of systematic variation names, excluding 'nominal'.
68
+ """
69
+ # Get systematics from metadata stored during schema building
70
+ return [
71
+ getattr(self, systematic)
72
+ for systematic in self.systematic_names
73
+ if systematic != "NOSYS"
74
+ ]
75
+
76
+
77
+ behavior["NtupleEvents"] = NtupleEvents
78
+
79
+
80
+ class NtupleEventsArray(behavior[("*", "NanoEvents")]): # type: ignore[misc, valid-type, name-defined]
81
+ """Collection of NtupleEvents objects, one for each systematic variation."""
82
+
83
+ def __getitem__(self, key):
84
+ """Support accessing systematic variations via bracket notation.
85
+
86
+ Args:
87
+ key: The systematic variation name. "NOSYS" returns the nominal events.
88
+
89
+ Returns:
90
+ The requested systematic variation or nominal events for "NOSYS".
91
+ """
92
+ if key == "NOSYS":
93
+ return self
94
+ return super().__getitem__(key)
95
+
96
+ @property
97
+ def systematic_names(self):
98
+ """Get all systematic variations available in this event collection.
99
+
100
+ Returns a list of systematic variation names, including 'NOSYS' for nominal.
101
+ """
102
+ # Get systematics from metadata stored during schema building
103
+ systematics = self.metadata.get("systematics", [])
104
+ return ["NOSYS", *systematics]
105
+
106
+ @property
107
+ def systematics(self):
108
+ """Get all systematic variations available in this event collection.
109
+
110
+ Returns a list of systematic variation names, excluding 'nominal'.
111
+ """
112
+ # Get systematics from metadata stored during schema building
113
+ return [
114
+ getattr(self, systematic)
115
+ for systematic in self.systematic_names
116
+ if systematic != "NOSYS"
117
+ ]
118
+
119
+
120
+ behavior[("*", "NtupleEvents")] = NtupleEventsArray
121
+
122
+
123
+ @awkward.mixin_class(behavior)
124
+ class Systematic(base.NanoCollection, base.Systematic):
125
+ """Base class for systematic variations."""
126
+
127
+ @property
128
+ def metadata(self):
129
+ """Arbitrary metadata"""
130
+ return self.layout.purelist_parameter("metadata") # pylint: disable=no-member
131
+
132
+ @property
133
+ def systematic(self):
134
+ """Get the systematic variation name for this event collection."""
135
+ return self.metadata["systematic"]
136
+
137
+ def __repr__(self):
138
+ return f"<event {self.systematic}>"
139
+
140
+
141
+ _set_repr_name("Systematic")
38
142
 
39
143
 
40
144
  @awkward.mixin_class(behavior)
@@ -51,7 +155,7 @@ class Pass(base.NanoCollection, base.Systematic): ...
51
155
  _set_repr_name("Pass")
52
156
 
53
157
  behavior.update(
54
- awkward._util.copy_behaviors("PtEtaPhiMLorentzVector", "Particle", behavior)
158
+ awkward._util.copy_behaviors("PtEtaPhiMLorentzVector", "Particle", behavior) # pylint: disable=protected-access
55
159
  )
56
160
 
57
161
 
@@ -63,22 +167,9 @@ class Particle(vector.PtEtaPhiMLorentzVector):
63
167
  - '{obj}_select'
64
168
  """
65
169
 
66
- @property
67
- def mass(self):
68
- r"""Invariant mass (+, -, -, -)
69
-
70
- :math:`\sqrt{t^2-x^2-y^2-z^2}`
71
- """
72
- return self["mass"] / 1.0e3
73
-
74
- @dask_method
75
170
  def passes(self, name):
76
171
  return self[f"select_{name}"] == 1
77
172
 
78
- @passes.dask
79
- def passes(self, dask_array, name):
80
- return dask_array[f"select_{name}"] == 1
81
-
82
173
  # NB: fields with the name 'pt' take precedence over this
83
174
  # @dask_property
84
175
  # def pt(self):
@@ -102,17 +193,19 @@ class Particle(vector.PtEtaPhiMLorentzVector):
102
193
 
103
194
  _set_repr_name("Particle")
104
195
 
105
- ParticleArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821
106
- ParticleArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821
107
- ParticleArray.ProjectionClass4D = ParticleArray # noqa: F821
108
- ParticleArray.MomentumClass = vector.LorentzVectorArray # noqa: F821
196
+ ParticleArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
197
+ ParticleArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
198
+ ParticleArray.ProjectionClass4D = ParticleArray # noqa: F821 # pylint: disable=undefined-variable
199
+ ParticleArray.MomentumClass = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
109
200
 
110
201
 
111
- behavior.update(awkward._util.copy_behaviors("PolarTwoVector", "MissingET", behavior))
202
+ behavior.update(awkward._util.copy_behaviors("PolarTwoVector", "MissingET", behavior)) # pylint: disable=protected-access
112
203
 
113
204
 
114
205
  @awkward.mixin_class(behavior)
115
206
  class MissingET(vector.PolarTwoVector, base.NanoCollection, base.Systematic):
207
+ """Missing transverse energy collection."""
208
+
116
209
  @property
117
210
  def r(self):
118
211
  """Distance from origin in XY plane"""
@@ -121,16 +214,18 @@ class MissingET(vector.PolarTwoVector, base.NanoCollection, base.Systematic):
121
214
 
122
215
  _set_repr_name("MissingET")
123
216
 
124
- MissingETArray.ProjectionClass2D = MissingETArray # noqa: F821
125
- MissingETArray.ProjectionClass3D = vector.SphericalThreeVectorArray # noqa: F821
126
- MissingETArray.ProjectionClass4D = vector.LorentzVectorArray # noqa: F821
127
- MissingETArray.MomentumClass = MissingETArray # noqa: F821
217
+ MissingETArray.ProjectionClass2D = MissingETArray # noqa: F821 # pylint: disable=undefined-variable
218
+ MissingETArray.ProjectionClass3D = vector.SphericalThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
219
+ MissingETArray.ProjectionClass4D = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
220
+ MissingETArray.MomentumClass = MissingETArray # noqa: F821 # pylint: disable=undefined-variable
128
221
 
129
- behavior.update(awkward._util.copy_behaviors("Particle", "Photon", behavior))
222
+ behavior.update(awkward._util.copy_behaviors("Particle", "Photon", behavior)) # pylint: disable=protected-access
130
223
 
131
224
 
132
225
  @awkward.mixin_class(behavior)
133
226
  class Photon(Particle, base.NanoCollection, base.Systematic):
227
+ """Photon particle collection."""
228
+
134
229
  @property
135
230
  def mass(self):
136
231
  """Return zero mass for photon."""
@@ -143,111 +238,126 @@ class Photon(Particle, base.NanoCollection, base.Systematic):
143
238
 
144
239
  @property
145
240
  def isEM(self):
146
- return self.isEM_syst.NOSYS == 0
241
+ return self.isEM_syst.NOSYS == 0 # pylint: disable=no-member
147
242
 
148
243
  def pass_isEM(self, words: list[PhotonID]):
149
244
  # 0 is pass, 1 is fail
150
245
  return (
151
- self.isEM_syst.NOSYS & reduce(ior, (1 << word.value for word in words))
246
+ self.isEM_syst.NOSYS & reduce(ior, (1 << word.value for word in words)) # pylint: disable=no-member
152
247
  ) == 0
153
248
 
154
249
 
155
250
  _set_repr_name("Photon")
156
251
 
157
- PhotonArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821
158
- PhotonArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821
159
- PhotonArray.ProjectionClass4D = PhotonArray # noqa: F821
160
- PhotonArray.MomentumClass = vector.LorentzVectorArray # noqa: F821
252
+ PhotonArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
253
+ PhotonArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
254
+ PhotonArray.ProjectionClass4D = PhotonArray # noqa: F821 # pylint: disable=undefined-variable
255
+ PhotonArray.MomentumClass = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
161
256
 
162
- behavior.update(awkward._util.copy_behaviors("Particle", "Electron", behavior))
257
+ behavior.update(awkward._util.copy_behaviors("Particle", "Electron", behavior)) # pylint: disable=protected-access
163
258
 
164
259
 
165
260
  @awkward.mixin_class(behavior)
166
261
  class Electron(Particle, base.NanoCollection, base.Systematic):
262
+ """Electron particle collection."""
263
+
167
264
  @property
168
265
  def mass(self):
169
- """Electron mass in GeV"""
170
- return particle.literals.e_minus.mass / 1.0e3
266
+ """Electron mass in MeV"""
267
+ return awkward.ones_like(self.pt) * particle.literals.e_minus.mass # pylint: disable=no-member
171
268
 
172
269
 
173
270
  _set_repr_name("Electron")
174
271
 
175
- ElectronArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821
176
- ElectronArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821
177
- ElectronArray.ProjectionClass4D = ElectronArray # noqa: F821
178
- ElectronArray.MomentumClass = vector.LorentzVectorArray # noqa: F821
272
+ ElectronArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
273
+ ElectronArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
274
+ ElectronArray.ProjectionClass4D = ElectronArray # noqa: F821 # pylint: disable=undefined-variable
275
+ ElectronArray.MomentumClass = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
179
276
 
180
- behavior.update(awkward._util.copy_behaviors("Particle", "Muon", behavior))
277
+ behavior.update(awkward._util.copy_behaviors("Particle", "Muon", behavior)) # pylint: disable=protected-access
181
278
 
182
279
 
183
280
  @awkward.mixin_class(behavior)
184
281
  class Muon(Particle, base.NanoCollection, base.Systematic):
282
+ """Muon particle collection."""
283
+
185
284
  @property
186
285
  def mass(self):
187
- """Muon mass in GeV"""
188
- return particle.literals.mu_minus.mass / 1.0e3
286
+ """Muon mass in MeV"""
287
+ return awkward.ones_like(self.pt) * particle.literals.mu_minus.mass # pylint: disable=no-member
189
288
 
190
289
 
191
290
  _set_repr_name("Muon")
192
291
 
193
- MuonArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821
194
- MuonArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821
195
- MuonArray.ProjectionClass4D = MuonArray # noqa: F821
196
- MuonArray.MomentumClass = vector.LorentzVectorArray # noqa: F821
292
+ MuonArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
293
+ MuonArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
294
+ MuonArray.ProjectionClass4D = MuonArray # noqa: F821 # pylint: disable=undefined-variable
295
+ MuonArray.MomentumClass = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
197
296
 
198
- behavior.update(awkward._util.copy_behaviors("Particle", "Tau", behavior))
297
+ behavior.update(awkward._util.copy_behaviors("Particle", "Tau", behavior)) # pylint: disable=protected-access
199
298
 
200
299
 
201
300
  @awkward.mixin_class(behavior)
202
301
  class Tau(Particle, base.NanoCollection, base.Systematic):
302
+ """Tau particle collection."""
303
+
203
304
  @property
204
305
  def mass(self):
205
- """Tau mass in GeV"""
206
- return particle.literals.tau_minus.mass / 1.0e3
306
+ """Tau mass in MeV"""
307
+ return awkward.ones_like(self.pt) * particle.literals.tau_minus.mass # pylint: disable=no-member
207
308
 
208
309
 
209
310
  _set_repr_name("Tau")
210
311
 
211
- TauArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821
212
- TauArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821
213
- TauArray.ProjectionClass4D = TauArray # noqa: F821
214
- TauArray.MomentumClass = vector.LorentzVectorArray # noqa: F821
312
+ TauArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
313
+ TauArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
314
+ TauArray.ProjectionClass4D = TauArray # noqa: F821 # pylint: disable=undefined-variable
315
+ TauArray.MomentumClass = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
215
316
 
216
317
 
217
- behavior.update(awkward._util.copy_behaviors("Particle", "Jet", behavior))
318
+ behavior.update(awkward._util.copy_behaviors("Particle", "Jet", behavior)) # pylint: disable=protected-access
218
319
 
219
320
 
220
321
  @awkward.mixin_class(behavior)
221
- class Jet(Particle, base.NanoCollection, base.Systematic): ...
322
+ class Jet(Particle, base.NanoCollection, base.Systematic):
323
+ """Jet particle collection."""
324
+
325
+ @property
326
+ def mass(self):
327
+ r"""Invariant mass (+, -, -, -)
328
+
329
+ :math:`\sqrt{t^2-x^2-y^2-z^2}`
330
+ """
331
+ return self["m"]
222
332
 
223
333
 
224
334
  _set_repr_name("Jet")
225
335
 
226
- JetArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821
227
- JetArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821
228
- JetArray.ProjectionClass4D = JetArray # noqa: F821
229
- JetArray.MomentumClass = vector.LorentzVectorArray # noqa: F821
336
+ JetArray.ProjectionClass2D = vector.TwoVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
337
+ JetArray.ProjectionClass3D = vector.ThreeVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
338
+ JetArray.ProjectionClass4D = JetArray # noqa: F821 # pylint: disable=undefined-variable
339
+ JetArray.MomentumClass = vector.LorentzVectorArray # noqa: F821 # pylint: disable=undefined-variable,no-member
230
340
 
231
341
  __all__ = [
232
342
  "Electron",
233
- "ElectronArray", # noqa: F822
234
- "ElectronRecord", # noqa: F822
343
+ "ElectronArray", # noqa: F822 # pylint: disable=undefined-all-variable
344
+ "ElectronRecord", # noqa: F822 # pylint: disable=undefined-all-variable
235
345
  "Jet",
236
- "JetArray", # noqa: F822
237
- "JetRecord", # noqa: F822
346
+ "JetArray", # noqa: F822 # pylint: disable=undefined-all-variable
347
+ "JetRecord", # noqa: F822 # pylint: disable=undefined-all-variable
238
348
  "MissingET",
239
- "MissingETArray", # noqa: F822
240
- "MissingETRecord", # noqa: F822
349
+ "MissingETArray", # noqa: F822 # pylint: disable=undefined-all-variable
350
+ "MissingETRecord", # noqa: F822 # pylint: disable=undefined-all-variable
241
351
  "Muon",
242
- "MuonArray", # noqa: F822
243
- "MuonRecord", # noqa: F822
352
+ "MuonArray", # noqa: F822 # pylint: disable=undefined-all-variable
353
+ "MuonRecord", # noqa: F822 # pylint: disable=undefined-all-variable
244
354
  "NtupleEvents",
245
355
  "Particle",
246
- "ParticleArray", # noqa: F822
247
- "ParticleRecord", # noqa: F822
356
+ "ParticleArray", # noqa: F822 # pylint: disable=undefined-all-variable
357
+ "ParticleRecord", # noqa: F822 # pylint: disable=undefined-all-variable
248
358
  "Pass",
249
359
  "Photon",
250
- "PhotonArray", # noqa: F822
251
- "PhotonRecord", # noqa: F822
360
+ "PhotonArray", # noqa: F822 # pylint: disable=undefined-all-variable
361
+ "PhotonRecord", # noqa: F822 # pylint: disable=undefined-all-variable
252
362
  "Weight",
253
363
  ]
atlas_schema/schema.py CHANGED
@@ -7,6 +7,7 @@ from typing import Any, ClassVar
7
7
 
8
8
  from coffea.nanoevents.schemas.base import BaseSchema, zip_forms
9
9
 
10
+ from atlas_schema.methods import behavior as roaster
10
11
  from atlas_schema.typing_compat import Behavior, Self
11
12
 
12
13
 
@@ -170,10 +171,12 @@ class NtupleSchema(BaseSchema): # type: ignore[misc]
170
171
  pass
171
172
  else:
172
173
  pass
173
- self._form["fields"], self._form["contents"] = self._build_collections(
174
- self._form["fields"], self._form["contents"]
174
+ self._form["fields"], self._form["contents"], discovered_systematics = (
175
+ self._build_collections(self._form["fields"], self._form["contents"])
175
176
  )
176
177
  self._form["parameters"]["metadata"]["version"] = self._version
178
+ self._form["parameters"]["metadata"]["systematics"] = discovered_systematics
179
+ self._form["parameters"]["__record__"] = "NtupleEvents"
177
180
 
178
181
  @classmethod
179
182
  def v1(cls, base_form: dict[str, Any]) -> Self:
@@ -186,11 +189,13 @@ class NtupleSchema(BaseSchema): # type: ignore[misc]
186
189
 
187
190
  def _build_collections(
188
191
  self, field_names: list[str], input_contents: list[Any]
189
- ) -> tuple[KeysView[str], ValuesView[dict[str, Any]]]:
192
+ ) -> tuple[KeysView[str], ValuesView[dict[str, Any]], list[str]]:
190
193
  branch_forms = dict(zip(field_names, input_contents))
191
194
 
192
195
  # parse into high-level records (collections, list collections, and singletons)
193
- collections = {k.split("_")[0] for k in branch_forms}
196
+ collections = {
197
+ k.split("_")[0] for k in branch_forms if k not in self.singletons
198
+ }
194
199
  collections -= self.event_ids
195
200
  collections -= set(self.singletons)
196
201
 
@@ -227,17 +232,38 @@ class NtupleSchema(BaseSchema): # type: ignore[misc]
227
232
  branch_forms[k.replace("_NOSYS", "") + "_NOSYS"] = branch_forms.pop(k)
228
233
 
229
234
  # these are collections with systematic variations
230
- subcollections = {
231
- k.split("__")[0].split("_", 1)[1].replace("_NOSYS", "")
232
- for k in branch_forms
233
- if "NOSYS" in k
234
- }
235
+ try:
236
+ subcollections = {
237
+ k.split("__")[0].split("_", 1)[1].replace("_NOSYS", "")
238
+ for k in branch_forms
239
+ if "NOSYS" in k and k not in self.singletons
240
+ }
241
+ except IndexError as exc:
242
+ msg = "One of the branches does not follow the assumed pattern for this schema. [invalid-branch-name]"
243
+ raise RuntimeError(msg) from exc
244
+
245
+ all_systematics = self._discover_systematics(
246
+ branch_forms, collections, subcollections
247
+ )
248
+
249
+ # Pre-compute systematic branch patterns for O(1) lookups
250
+ # This replaces the expensive O(m*s) nested condition checks
251
+ systematic_branch_patterns = set()
252
+ for collection in collections:
253
+ for subcoll in subcollections:
254
+ for sys in all_systematics:
255
+ if sys != "NOSYS":
256
+ systematic_branch_patterns.add(f"{collection}_{subcoll}_{sys}")
235
257
 
236
258
  # Check the presence of the event_ids
237
259
  missing_event_ids = [
238
260
  event_id for event_id in self.event_ids if event_id not in branch_forms
239
261
  ]
240
262
 
263
+ missing_singletons = [
264
+ singleton for singleton in self.singletons if singleton not in branch_forms
265
+ ]
266
+
241
267
  if len(missing_event_ids) > 0:
242
268
  if self.error_missing_event_ids:
243
269
  msg = f"There are missing event ID fields: {missing_event_ids} \n\n\
@@ -253,99 +279,227 @@ class NtupleSchema(BaseSchema): # type: ignore[misc]
253
279
  stacklevel=2,
254
280
  )
255
281
 
282
+ if len(missing_singletons) > 0:
283
+ # These singletons are simply branches we do not parse or handle
284
+ # explicitly in atlas-schema (e.g. they are copied directly to the
285
+ # output structure we provide you), however there can be false
286
+ # positives when you submit multiple files with different branch
287
+ # structures and this warning could be safely ignored.
288
+ warnings.warn(
289
+ f"Missing singletons : {missing_singletons}. [singleton-missing]",
290
+ RuntimeWarning,
291
+ stacklevel=2,
292
+ )
293
+
256
294
  output = {}
257
295
 
258
296
  # first, register singletons (event-level, others)
259
297
  for name in {*self.event_ids, *self.singletons}:
260
- if name in missing_event_ids:
298
+ if name in [*missing_event_ids, *missing_singletons]:
261
299
  continue
300
+
262
301
  output[name] = branch_forms[name]
263
302
 
264
- # next, go through and start grouping up collections
265
- for name in collections:
266
- content = {}
303
+ # First, build nominal collections the traditional way
304
+ nominal_collections = {}
305
+ for collection_name in collections:
306
+ collection_content = {}
267
307
  used = set()
268
308
 
309
+ # Process subcollections with NOSYS variations
269
310
  for subname in subcollections:
270
- prefix = f"{name}_{subname}_"
271
- used.update({k for k in branch_forms if k.startswith(prefix)})
272
- subcontent = {
273
- k[len(prefix) :]: branch_forms[k]
274
- for k in branch_forms
275
- if k.startswith(prefix)
276
- }
277
- if subcontent:
278
- # create the nominal version
279
- content[subname] = branch_forms[f"{prefix}NOSYS"]
280
- # create a collection of the systematic variations for the given variable
281
- content[f"{subname}_syst"] = zip_forms(
282
- subcontent, f"{name}_syst", record_name="NanoCollection"
311
+ prefix = f"{collection_name}_{subname}_"
312
+ nosys_branch = f"{prefix}NOSYS"
313
+
314
+ if nosys_branch in branch_forms:
315
+ collection_content[subname] = branch_forms[nosys_branch]
316
+ used.add(nosys_branch)
317
+
318
+ # Add non-systematic branches (like eta, phi that don't vary)
319
+ for k, form in branch_forms.items():
320
+ if (
321
+ k.startswith(collection_name + "_")
322
+ and k not in used
323
+ and "_NOSYS" not in k
324
+ and k
325
+ not in systematic_branch_patterns # O(1) lookup instead of O(m*s)
326
+ ):
327
+ field_name = k[len(collection_name) + 1 :]
328
+ if field_name not in collection_content:
329
+ collection_content[field_name] = form
330
+
331
+ if collection_content:
332
+ behavior = self.mixins.get(collection_name, "")
333
+ if not behavior:
334
+ behavior = self.suggested_behavior(collection_name)
335
+ warnings.warn(
336
+ f"I found a collection with no defined mixin: '{collection_name}'. I will assume behavior: '{behavior}'. To suppress this warning next time, please define mixins for your custom collections. [mixin-undefined]",
337
+ RuntimeWarning,
338
+ stacklevel=2,
283
339
  )
340
+ nominal_collections[collection_name] = zip_forms(
341
+ collection_content, collection_name, record_name=behavior
342
+ )
343
+ nominal_collections[collection_name].setdefault("parameters", {})
344
+ nominal_collections[collection_name]["parameters"].update(
345
+ {"collection_name": collection_name}
346
+ )
284
347
 
285
- content.update(
286
- {
287
- k[len(name) + 1 :]: branch_forms[k]
288
- for k in branch_forms
289
- if k.startswith(name + "_") and k not in used
348
+ # Add nominal collections to output
349
+ output.update(nominal_collections)
350
+
351
+ # Now build systematic event structures
352
+ for systematic in all_systematics:
353
+ if systematic == "NOSYS":
354
+ continue
355
+
356
+ # Check which collections actually have this systematic variation
357
+ systematic_collections = {}
358
+
359
+ for collection_name in collections:
360
+ # Check if this collection has any systematic branches for this systematic
361
+ has_systematic_data = False
362
+ collection_content = {}
363
+ used = set()
364
+
365
+ # Process subcollections with systematic variations
366
+ for subname in subcollections:
367
+ prefix = f"{collection_name}_{subname}_"
368
+ target_branch = f"{prefix}{systematic}"
369
+ fallback_branch = f"{prefix}NOSYS"
370
+
371
+ if target_branch in branch_forms:
372
+ # Use the systematic variation
373
+ collection_content[subname] = branch_forms[target_branch]
374
+ used.add(target_branch)
375
+ has_systematic_data = True
376
+ elif fallback_branch in branch_forms:
377
+ # Fall back to nominal
378
+ collection_content[subname] = branch_forms[fallback_branch]
379
+ used.add(fallback_branch)
380
+
381
+ # Add non-systematic branches
382
+ for k, form in branch_forms.items():
383
+ if (
384
+ k.startswith(collection_name + "_")
385
+ and k not in used
386
+ and "_NOSYS" not in k
387
+ and k
388
+ not in systematic_branch_patterns # O(1) lookup instead of O(m*s)
389
+ ):
390
+ field_name = k[len(collection_name) + 1 :]
391
+ if field_name not in collection_content:
392
+ collection_content[field_name] = form
393
+
394
+ # If this collection has systematic data or fallback data, include it
395
+ if collection_content:
396
+ behavior = self.mixins.get(collection_name, "")
397
+ if not behavior:
398
+ behavior = self.suggested_behavior(collection_name)
399
+ # Only warn once (for nominal collections)
400
+
401
+ # If no systematic data, use the nominal collection directly
402
+ if (
403
+ not has_systematic_data
404
+ and collection_name in nominal_collections
405
+ ):
406
+ systematic_collections[collection_name] = nominal_collections[
407
+ collection_name
408
+ ]
409
+ else:
410
+ # Build the systematic collection
411
+ systematic_collections[collection_name] = zip_forms(
412
+ collection_content, collection_name, record_name=behavior
413
+ )
414
+ systematic_collections[collection_name].setdefault(
415
+ "parameters", {}
416
+ )
417
+ systematic_collections[collection_name]["parameters"].update(
418
+ {"collection_name": collection_name}
419
+ )
420
+
421
+ # Only create systematic event if there are collections for it
422
+ if systematic_collections:
423
+ output[systematic] = {
424
+ "class": "RecordArray",
425
+ "contents": list(systematic_collections.values()),
426
+ "fields": list(systematic_collections.keys()),
427
+ "form_key": f"%21invalid%2C{systematic}",
428
+ "parameters": {
429
+ "__record__": "Systematic",
430
+ "metadata": {"systematic": systematic},
431
+ },
290
432
  }
291
- )
292
433
 
293
- if not used and not content:
434
+ # Handle any remaining unrecognized branches as singletons
435
+ processed_branches = set()
436
+ # Add event IDs and explicit singletons
437
+ processed_branches.update(self.event_ids)
438
+ processed_branches.update(self.singletons)
439
+ # Add collection-related branches
440
+ for collection_name in collections:
441
+ for branch_name in branch_forms:
442
+ if branch_name.startswith(collection_name + "_"):
443
+ processed_branches.add(branch_name)
444
+
445
+ # Find unrecognized branches
446
+ for branch_name, form in branch_forms.items():
447
+ if branch_name not in processed_branches:
448
+ # This is an unrecognized branch - treat as singleton with warning
294
449
  warnings.warn(
295
- f"I identified a branch that likely does not have any leaves: '{name}'. I will treat this as a 'singleton'. To suppress this warning next time, please define your singletons explicitly. [singleton-undefined]",
450
+ f"I identified a branch that likely does not have any leaves: '{branch_name}'. I will treat this as a 'singleton'. To suppress this warning, add this branch to the singletons set. [singleton-undefined]",
296
451
  RuntimeWarning,
297
452
  stacklevel=2,
298
453
  )
299
- self.singletons.add(name)
300
- output[name] = branch_forms[name]
454
+ output[branch_name] = form
301
455
 
302
- else:
303
- behavior = self.mixins.get(name, "")
304
- if not behavior:
305
- behavior = self.suggested_behavior(name)
306
- warnings.warn(
307
- f"I found a collection with no defined mixin: '{name}'. I will assume behavior: '{behavior}'. To suppress this warning next time, please define mixins for your custom collections. [mixin-undefined]",
308
- RuntimeWarning,
309
- stacklevel=2,
310
- )
456
+ # Return discovered systematics (excluding NOSYS/nominal)
457
+ discovered_systematics = sorted([s for s in all_systematics if s != "NOSYS"])
311
458
 
312
- output[name] = zip_forms(content, name, record_name=behavior)
313
-
314
- output[name].setdefault("parameters", {})
315
- output[name]["parameters"].update({"collection_name": name})
316
-
317
- if output[name]["class"] == "ListOffsetArray":
318
- if output[name]["class"] == "RecordArray":
319
- parameters = output[name]["content"]["fields"]
320
- contents = output[name]["content"]["contents"]
321
- else:
322
- # these are also singletons of another kind that we just pass through
323
- continue
324
- elif output[name]["class"] == "RecordArray":
325
- parameters = output[name]["fields"]
326
- contents = output[name]["contents"]
327
- elif output[name]["class"] == "NumpyArray":
328
- # these are singletons that we just pass through
329
- continue
330
- else:
331
- msg = f"Unhandled class {output[name]['class']}"
332
- raise RuntimeError(msg)
459
+ return output.keys(), output.values(), discovered_systematics
333
460
 
334
- # update docstrings as needed
335
- # NB: must be before flattening for easier logic
336
- for index, parameter in enumerate(parameters):
337
- if "parameters" not in contents[index]:
338
- continue
339
-
340
- parsed_name = parameter.replace("_NOSYS", "")
341
- contents[index]["parameters"]["__doc__"] = self.docstrings.get(
342
- parsed_name,
343
- contents[index]["parameters"].get(
344
- "__doc__", "no docstring available"
345
- ),
346
- )
461
+ def _discover_systematics(
462
+ self,
463
+ branch_forms: dict[str, Any],
464
+ collections: set[str],
465
+ subcollections: set[str],
466
+ ) -> set[str]:
467
+ """Extract systematic variations from branch names.
468
+
469
+ Returns:
470
+ set: Set of all systematic variation names found in branches
471
+ """
472
+ # Optimize systematic discovery: pre-index branches by pattern
473
+ # This avoids O(n*m) nested loops in systematic discovery
474
+ subcoll_patterns = {f"{subcoll}_" for subcoll in subcollections}
475
+
476
+ all_systematics = set()
477
+ for k in branch_forms:
478
+ if not ("_" in k and k not in self.singletons):
479
+ continue
480
+ # Handle the pattern: collection_subcollection_systematic
481
+ # where systematic can contain double underscores like "JET_EnergyResolution__1up"
482
+ parts = k.split("_")
483
+ if len(parts) < 3:
484
+ continue
485
+ # Find the collection and subcollection parts
486
+ collection = parts[0]
487
+ if collection not in collections:
488
+ continue
489
+ # Find where the subcollection ends by looking for a known pattern
490
+ # The systematic starts after the subcollection
491
+ remaining = "_".join(parts[1:])
492
+ # Use optimized lookup instead of iterating all subcollections
493
+ for pattern in subcoll_patterns:
494
+ if remaining.startswith(pattern):
495
+ systematic = remaining[len(pattern) :]
496
+ if systematic and systematic != "NOSYS":
497
+ all_systematics.add(systematic)
498
+ break
347
499
 
348
- return output.keys(), output.values()
500
+ # Always include NOSYS as the nominal case
501
+ all_systematics.add("NOSYS")
502
+ return all_systematics
349
503
 
350
504
  @classmethod
351
505
  def behavior(cls) -> Behavior:
@@ -354,8 +508,6 @@ class NtupleSchema(BaseSchema): # type: ignore[misc]
354
508
  Returns:
355
509
  dict[str | tuple['*', str], type[awkward.Record]]: an :data:`awkward.behavior` dictionary
356
510
  """
357
- from atlas_schema.methods import behavior as roaster
358
-
359
511
  return roaster
360
512
 
361
513
  @classmethod
atlas_schema/utils.py CHANGED
@@ -1,16 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
- from typing import TypeVar, Union, cast
4
+ from typing import TypeVar, cast
5
5
 
6
6
  import awkward as ak
7
- import dask_awkward as dak
8
7
 
9
- Array = TypeVar("Array", bound=Union[dak.Array, ak.Array])
8
+ Array = TypeVar("Array", bound=ak.Array)
10
9
  _E = TypeVar("_E", bound=Enum)
11
10
 
12
11
 
13
- def isin(element: Array, test_elements: dak.Array | ak.Array, axis: int = -1) -> Array:
12
+ def isin(element: Array, test_elements: ak.Array, axis: int = -1) -> Array:
14
13
  """
15
14
  Find test_elements in element. Similar in API as :func:`numpy.isin`.
16
15
 
@@ -21,12 +20,12 @@ def isin(element: Array, test_elements: dak.Array | ak.Array, axis: int = -1) ->
21
20
  comparison.
22
21
 
23
22
  Args:
24
- element (dask_awkward.Array or ak.Array): input array of values.
25
- test_elements (dask_awkward.Array or ak.Array): one-dimensional set of values against which to test each value of *element*.
23
+ element (ak.Array): input array of values.
24
+ test_elements (ak.Array): one-dimensional set of values against which to test each value of *element*.
26
25
  axis (int): the axis along which the comparison is performed
27
26
 
28
27
  Returns:
29
- dask_awkward.Array or ak.Array: result of comparison for test_elements in *element*
28
+ ak.Array: result of comparison for test_elements in *element*
30
29
 
31
30
  Example:
32
31
  >>> import awkward as ak
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atlas-schema
3
- Version: 0.2.4
3
+ Version: 0.4.0
4
4
  Summary: Helper python package for ATLAS Common NTuple Analysis work.
5
5
  Project-URL: Homepage, https://github.com/scipp-atlas/atlas-schema
6
6
  Project-URL: Bug Tracker, https://github.com/scipp-atlas/atlas-schema/issues
7
7
  Project-URL: Discussions, https://github.com/scipp-atlas/atlas-schema/discussions
8
- Project-URL: Documentation, https://atlas-schema.readthedocs.io/en/v0.2.4/
8
+ Project-URL: Documentation, https://atlas-schema.readthedocs.io/en/v0.4.0/
9
9
  Project-URL: Releases, https://github.com/scipp-atlas/atlas-schema/releases
10
10
  Project-URL: Release Notes, https://atlas-schema.readthedocs.io/en/latest/history.html
11
11
  Author-email: Giordon Stark <kratsg@gmail.com>
@@ -227,7 +227,7 @@ Classifier: Programming Language :: Python :: 3.12
227
227
  Classifier: Topic :: Scientific/Engineering
228
228
  Classifier: Typing :: Typed
229
229
  Requires-Python: >=3.9
230
- Requires-Dist: coffea[dask]>=2024.4.1
230
+ Requires-Dist: coffea[dask]>=2025.7.0
231
231
  Requires-Dist: particle>=0.25.0
232
232
  Provides-Extra: dev
233
233
  Requires-Dist: pytest-cov>=3; extra == 'dev'
@@ -251,7 +251,7 @@ Requires-Dist: tbump>=6.7.0; extra == 'test'
251
251
  Requires-Dist: twine; extra == 'test'
252
252
  Description-Content-Type: text/markdown
253
253
 
254
- # atlas-schema v0.2.4
254
+ # atlas-schema v0.4.0
255
255
 
256
256
  [![Actions Status][actions-badge]][actions-link]
257
257
  [![Documentation Status][rtd-badge]][rtd-link]
@@ -335,11 +335,9 @@ like below:
335
335
 
336
336
  ```python
337
337
  import awkward as ak
338
- import dask
339
- import hist.dask as had
338
+ from hist import Hist
340
339
  import matplotlib.pyplot as plt
341
340
  from coffea import processor
342
- from coffea.nanoevents import NanoEventsFactory
343
341
  from distributed import Client
344
342
 
345
343
  from atlas_schema.schema import NtupleSchema
@@ -352,7 +350,7 @@ class MyFirstProcessor(processor.ProcessorABC):
352
350
  def process(self, events):
353
351
  dataset = events.metadata["dataset"]
354
352
  h_ph_pt = (
355
- had.Hist.new.StrCat(["all", "pass", "fail"], name="isEM")
353
+ Hist.new.StrCat(["all", "pass", "fail"], name="isEM")
356
354
  .Regular(200, 0.0, 2000.0, name="pt", label="$pt_{\gamma}$ [GeV]")
357
355
  .Int64()
358
356
  )
@@ -376,17 +374,18 @@ class MyFirstProcessor(processor.ProcessorABC):
376
374
  if __name__ == "__main__":
377
375
  client = Client()
378
376
 
379
- fname = "ntuple.root"
380
- events = NanoEventsFactory.from_root(
381
- {fname: "analysis"},
382
- schemaclass=NtupleSchema,
383
- metadata={"dataset": "700352.Zqqgamma.mc20d.v1"},
384
- ).events()
377
+ fileset = {"700352.Zqqgamma.mc20d.v1": {"files": {"ntuple.root": "analysis"}}}
385
378
 
386
- p = MyFirstProcessor()
387
- out = p.process(events)
388
- (computed,) = dask.compute(out)
389
- print(computed)
379
+ run = processor.Runner(
380
+ executor=processor.IterativeExecutor(compression=None),
381
+ schema=NtupleSchema,
382
+ savemetrics=True,
383
+ )
384
+
385
+ out, metrics = run(fileset, processor_instance=MyFirstProcessor())
386
+
387
+ print(out)
388
+ print(metrics)
390
389
 
391
390
  fig, ax = plt.subplots()
392
391
  computed["700352.Zqqgamma.mc20d.v1"]["ph_pt"].plot1d(ax=ax)
@@ -400,6 +399,57 @@ which produces
400
399
 
401
400
  <img src="https://raw.githubusercontent.com/scipp-atlas/atlas-schema/main/docs/_static/img/ph_pt.png" alt="three stacked histograms of photon pT, with each stack corresponding to: no selection, requiring the isEM flag, and inverting the isEM requirement" width="500" style="display: block; margin-left: auto; margin-right: auto;">
402
401
 
402
+ ## Processing with Systematic Variations
403
+
404
+ For analyses requiring systematic uncertainty evaluation, you can easily iterate
405
+ over all systematic variations using the new `events["NOSYS"]` alias and
406
+ `systematic_names` property:
407
+
408
+ ```python
409
+ import awkward as ak
410
+ from hist import Hist
411
+ from coffea import processor
412
+ from atlas_schema.schema import NtupleSchema
413
+
414
+
415
+ class SystematicsProcessor(processor.ProcessorABC):
416
+ def __init__(self):
417
+ self.h = (
418
+ Hist.new.StrCat([], name="variation", growth=True)
419
+ .Regular(50, 0.0, 500.0, name="jet_pt", label="Leading Jet $p_T$ [GeV]")
420
+ .Int64()
421
+ )
422
+
423
+ def process(self, events):
424
+ dsid = events.metadata["dataset"]
425
+
426
+ # Process all systematic variations including nominal ("NOSYS")
427
+ for variation in events.systematic_names:
428
+ event_view = events[variation]
429
+
430
+ # Fill histogram with leading jet pT for this systematic variation
431
+ leading_jet_pt = event_view.jet.pt[:, 0] / 1_000 # Convert MeV to GeV
432
+ weights = (
433
+ event_view.weight.mc
434
+ if hasattr(event_view, "weight")
435
+ else ak.ones_like(leading_jet_pt)
436
+ )
437
+
438
+ self.h.fill(variation=variation, jet_pt=leading_jet_pt, weight=weights)
439
+
440
+ return {
441
+ "hist": self.h,
442
+ "meta": {"sumw": {dsid: {(events.metadata["fileuuid"], ak.sum(weights))}}},
443
+ }
444
+
445
+ def postprocess(self, accumulator):
446
+ return accumulator
447
+ ```
448
+
449
+ This approach allows you to seamlessly process both nominal and systematic
450
+ variations in a single loop, eliminating the need for special-case handling of
451
+ the nominal variation.
452
+
403
453
  <!-- SPHINX-END -->
404
454
 
405
455
  ## Developer Notes
@@ -0,0 +1,13 @@
1
+ atlas_schema/__init__.py,sha256=ebY-rTiwSGnfvt1yWATze2GE7K3fVgJj6fT64Sl4sH8,469
2
+ atlas_schema/_version.py,sha256=2_0GUP7yBCXRus-qiJKxQD62z172WSs1sQ6DVpPsbmM,704
3
+ atlas_schema/_version.pyi,sha256=j5kbzfm6lOn8BzASXWjGIA1yT0OlHTWqlbyZ8Si_o0E,118
4
+ atlas_schema/enums.py,sha256=GDDKSBZY-L8X5W41Kwi0G5Yd4Vu4Kiga-ttSbztEXEM,3687
5
+ atlas_schema/methods.py,sha256=rQRQgD26ndCzwpxAuAeEbXIHd8v64cK2rP5A5GxvBn8,12934
6
+ atlas_schema/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ atlas_schema/schema.py,sha256=TbNxekA0DRwS5Mye0frHo7p1K7LW7FBXrkEwmPUQ8MA,27884
8
+ atlas_schema/typing_compat.py,sha256=3G8h4WfLoDmrtWZvtYKLCwEpCQ_O4Fwygb2WlDRSE4E,488
9
+ atlas_schema/utils.py,sha256=E3jCka-pf_0h_r3OO0hMLlbF6dQKoxr2T1Gd18-aJ4U,2034
10
+ atlas_schema-0.4.0.dist-info/METADATA,sha256=9d1QN2OaZ0i68ZDAR3GBKUNw9Lm4l1nAJycCb9jSmqg,21755
11
+ atlas_schema-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ atlas_schema-0.4.0.dist-info/licenses/LICENSE,sha256=snem82NV8fgAi4DKaaUIfReaM5RqIWbH5OOXOvy40_w,11344
13
+ atlas_schema-0.4.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- atlas_schema/__init__.py,sha256=ebY-rTiwSGnfvt1yWATze2GE7K3fVgJj6fT64Sl4sH8,469
2
- atlas_schema/_version.py,sha256=4gL0W4-u58XR5lRLpeoIPrGhcewTk0-527de6uTNmkg,411
3
- atlas_schema/_version.pyi,sha256=j5kbzfm6lOn8BzASXWjGIA1yT0OlHTWqlbyZ8Si_o0E,118
4
- atlas_schema/enums.py,sha256=hwgOvFBmITNxL0MQkrNpbiPv9VMezFoE-eyGgjzem8E,3688
5
- atlas_schema/methods.py,sha256=hFdtKXnyCcx4M05WhAM24fKwzEhh_ubA7jNa6_xv67k,7238
6
- atlas_schema/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- atlas_schema/schema.py,sha256=4OAvuPrOds-taVES32y4K8dvNDf8PKdu83DZqAlTdp8,20621
8
- atlas_schema/typing_compat.py,sha256=3G8h4WfLoDmrtWZvtYKLCwEpCQ_O4Fwygb2WlDRSE4E,488
9
- atlas_schema/utils.py,sha256=IqMbWqq0ib_kZdJCaM5ghURZatmb8pKidlewx3dpy0A,2164
10
- atlas_schema-0.2.4.dist-info/METADATA,sha256=KZDH5fsZon5wFXuU-iSUeqgjoplOwAoqTM1I9LgaTiM,20107
11
- atlas_schema-0.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- atlas_schema-0.2.4.dist-info/licenses/LICENSE,sha256=snem82NV8fgAi4DKaaUIfReaM5RqIWbH5OOXOvy40_w,11344
13
- atlas_schema-0.2.4.dist-info/RECORD,,