atdata 0.2.3b1__py3-none-any.whl → 0.3.0b1__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.
Files changed (48) hide show
  1. atdata/.gitignore +1 -0
  2. atdata/__init__.py +30 -0
  3. atdata/_exceptions.py +168 -0
  4. atdata/_helpers.py +29 -15
  5. atdata/_hf_api.py +63 -11
  6. atdata/_logging.py +70 -0
  7. atdata/_protocols.py +19 -62
  8. atdata/_schema_codec.py +5 -4
  9. atdata/_type_utils.py +28 -2
  10. atdata/atmosphere/__init__.py +19 -9
  11. atdata/atmosphere/records.py +3 -2
  12. atdata/atmosphere/schema.py +2 -2
  13. atdata/cli/__init__.py +157 -171
  14. atdata/cli/inspect.py +69 -0
  15. atdata/cli/local.py +1 -1
  16. atdata/cli/preview.py +63 -0
  17. atdata/cli/schema.py +109 -0
  18. atdata/dataset.py +428 -326
  19. atdata/lens.py +9 -2
  20. atdata/local/__init__.py +71 -0
  21. atdata/local/_entry.py +157 -0
  22. atdata/local/_index.py +940 -0
  23. atdata/local/_repo_legacy.py +218 -0
  24. atdata/local/_s3.py +349 -0
  25. atdata/local/_schema.py +380 -0
  26. atdata/manifest/__init__.py +28 -0
  27. atdata/manifest/_aggregates.py +156 -0
  28. atdata/manifest/_builder.py +163 -0
  29. atdata/manifest/_fields.py +154 -0
  30. atdata/manifest/_manifest.py +146 -0
  31. atdata/manifest/_query.py +150 -0
  32. atdata/manifest/_writer.py +74 -0
  33. atdata/promote.py +4 -4
  34. atdata/providers/__init__.py +25 -0
  35. atdata/providers/_base.py +140 -0
  36. atdata/providers/_factory.py +69 -0
  37. atdata/providers/_postgres.py +214 -0
  38. atdata/providers/_redis.py +171 -0
  39. atdata/providers/_sqlite.py +191 -0
  40. atdata/repository.py +323 -0
  41. atdata/testing.py +337 -0
  42. {atdata-0.2.3b1.dist-info → atdata-0.3.0b1.dist-info}/METADATA +4 -1
  43. atdata-0.3.0b1.dist-info/RECORD +54 -0
  44. atdata/local.py +0 -1720
  45. atdata-0.2.3b1.dist-info/RECORD +0 -28
  46. {atdata-0.2.3b1.dist-info → atdata-0.3.0b1.dist-info}/WHEEL +0 -0
  47. {atdata-0.2.3b1.dist-info → atdata-0.3.0b1.dist-info}/entry_points.txt +0 -0
  48. {atdata-0.2.3b1.dist-info → atdata-0.3.0b1.dist-info}/licenses/LICENSE +0 -0
atdata/testing.py ADDED
@@ -0,0 +1,337 @@
1
+ """Testing utilities for atdata.
2
+
3
+ Provides mock clients, dataset factories, and pytest fixtures for writing
4
+ tests against atdata without requiring external services (Redis, S3, ATProto PDS).
5
+
6
+ Usage::
7
+
8
+ import atdata.testing as at_test
9
+
10
+ # Create a dataset from samples
11
+ ds = at_test.make_dataset(tmp_path, [sample1, sample2])
12
+
13
+ # Generate random samples
14
+ samples = at_test.make_samples(MyType, n=100)
15
+
16
+ # Use mock atmosphere client
17
+ client = at_test.MockAtmosphereClient()
18
+
19
+ # Use in-memory index (SQLite backed, temporary)
20
+ index = at_test.mock_index(tmp_path)
21
+
22
+ Pytest fixtures (available when ``atdata`` is installed)::
23
+
24
+ def test_something(mock_atmosphere):
25
+ client = mock_atmosphere
26
+ client.login("user", "pass")
27
+ ...
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import tempfile
33
+ import uuid
34
+ from dataclasses import fields as dc_fields
35
+ from pathlib import Path
36
+ from typing import Any, Sequence, Type, TypeVar
37
+
38
+ import numpy as np
39
+ import webdataset as wds
40
+
41
+ import atdata
42
+ from atdata import Dataset, PackableSample
43
+ from atdata.local._index import Index
44
+ from atdata.providers._sqlite import SqliteProvider
45
+
46
+ ST = TypeVar("ST")
47
+
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Mock Atmosphere Client
51
+ # ---------------------------------------------------------------------------
52
+
53
+
54
+ class MockAtmosphereClient:
55
+ """In-memory mock of ``AtmosphereClient`` for testing.
56
+
57
+ Simulates login, schema publishing, dataset publishing, and record
58
+ retrieval without requiring a live ATProto PDS.
59
+
60
+ Examples:
61
+ >>> client = MockAtmosphereClient()
62
+ >>> client.login("alice.test", "password")
63
+ >>> client.did
64
+ 'did:plc:mock000000000000'
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ did: str = "did:plc:mock000000000000",
70
+ handle: str = "test.mock.social",
71
+ ) -> None:
72
+ self.did = did
73
+ self.handle = handle
74
+ self._logged_in = False
75
+ self._records: dict[str, dict[str, Any]] = {}
76
+ self._schemas: dict[str, dict[str, Any]] = {}
77
+ self._datasets: dict[str, dict[str, Any]] = {}
78
+ self._blobs: dict[str, bytes] = {}
79
+ self._session_string = "mock-session-string"
80
+ self._call_log: list[tuple[str, dict[str, Any]]] = []
81
+
82
+ def login(self, handle: str, password: str) -> dict[str, Any]:
83
+ """Simulate login. Always succeeds."""
84
+ self._logged_in = True
85
+ self.handle = handle
86
+ self._call_log.append(("login", {"handle": handle}))
87
+ return {"did": self.did, "handle": self.handle}
88
+
89
+ @property
90
+ def is_authenticated(self) -> bool:
91
+ return self._logged_in
92
+
93
+ def export_session_string(self) -> str:
94
+ return self._session_string
95
+
96
+ def create_record(
97
+ self,
98
+ collection: str,
99
+ record: dict[str, Any],
100
+ rkey: str | None = None,
101
+ ) -> str:
102
+ """Simulate creating a record. Returns a mock AT URI."""
103
+ key = rkey or uuid.uuid4().hex[:12]
104
+ uri = f"at://{self.did}/{collection}/{key}"
105
+ self._records[uri] = record
106
+ self._call_log.append(
107
+ ("create_record", {"collection": collection, "rkey": key, "uri": uri})
108
+ )
109
+ return uri
110
+
111
+ def get_record(self, uri: str) -> dict[str, Any]:
112
+ """Retrieve a previously created record by URI."""
113
+ if uri not in self._records:
114
+ raise KeyError(f"Record not found: {uri}")
115
+ return self._records[uri]
116
+
117
+ def list_records(self, collection: str) -> list[dict[str, Any]]:
118
+ """List records for a collection."""
119
+ return [
120
+ {"uri": uri, "value": rec}
121
+ for uri, rec in self._records.items()
122
+ if collection in uri
123
+ ]
124
+
125
+ def upload_blob(self, data: bytes) -> dict[str, Any]:
126
+ """Simulate uploading a blob. Returns a mock blob ref."""
127
+ ref = f"blob:{uuid.uuid4().hex[:16]}"
128
+ self._blobs[ref] = data
129
+ self._call_log.append(("upload_blob", {"ref": ref, "size": len(data)}))
130
+ return {"ref": {"$link": ref}, "mimeType": "application/octet-stream"}
131
+
132
+ def get_blob(self, did: str, cid: str) -> bytes:
133
+ """Retrieve a previously uploaded blob."""
134
+ if cid not in self._blobs:
135
+ raise KeyError(f"Blob not found: {cid}")
136
+ return self._blobs[cid]
137
+
138
+ def reset(self) -> None:
139
+ """Clear all stored state."""
140
+ self._records.clear()
141
+ self._schemas.clear()
142
+ self._datasets.clear()
143
+ self._blobs.clear()
144
+ self._call_log.clear()
145
+ self._logged_in = False
146
+
147
+
148
+ # ---------------------------------------------------------------------------
149
+ # Dataset Factory
150
+ # ---------------------------------------------------------------------------
151
+
152
+
153
+ def make_dataset(
154
+ path: Path,
155
+ samples: Sequence[PackableSample],
156
+ *,
157
+ name: str = "test",
158
+ sample_type: type | None = None,
159
+ ) -> Dataset:
160
+ """Create a ``Dataset`` from a list of samples.
161
+
162
+ Writes the samples to a WebDataset tar file in *path* and returns a
163
+ ``Dataset`` configured to read them back.
164
+
165
+ Args:
166
+ path: Directory where the tar file will be created.
167
+ samples: Sequence of ``PackableSample`` (or ``@packable``) instances.
168
+ name: Filename prefix for the tar file.
169
+ sample_type: Explicit sample type for the Dataset generic parameter.
170
+ If ``None``, inferred from the first sample.
171
+
172
+ Returns:
173
+ A ``Dataset`` ready for iteration.
174
+
175
+ Examples:
176
+ >>> ds = make_dataset(tmp_path, [MySample(x=1), MySample(x=2)])
177
+ >>> assert len(list(ds.ordered())) == 2
178
+ """
179
+ if not samples:
180
+ raise ValueError("samples must be non-empty")
181
+
182
+ tar_path = path / f"{name}-000000.tar"
183
+ tar_path.parent.mkdir(parents=True, exist_ok=True)
184
+
185
+ with wds.writer.TarWriter(str(tar_path)) as writer:
186
+ for sample in samples:
187
+ writer.write(sample.as_wds)
188
+
189
+ st = sample_type or type(samples[0])
190
+ return Dataset[st](url=str(tar_path))
191
+
192
+
193
+ def make_samples(
194
+ sample_type: Type[ST], n: int = 10, seed: int | None = None
195
+ ) -> list[ST]:
196
+ """Generate *n* random instances of a ``@packable`` sample type.
197
+
198
+ Inspects the dataclass fields and generates appropriate random data:
199
+ - ``str`` fields get ``"field_name_0"``, ``"field_name_1"``, etc.
200
+ - ``int`` fields get sequential integers
201
+ - ``float`` fields get random floats in [0, 1)
202
+ - ``bool`` fields alternate True/False
203
+ - ``bytes`` fields get random 16 bytes
204
+ - NDArray fields get random ``(4, 4)`` float32 arrays
205
+
206
+ Args:
207
+ sample_type: A ``@packable``-decorated class or ``PackableSample`` subclass.
208
+ n: Number of samples to generate.
209
+ seed: Optional random seed for reproducibility.
210
+
211
+ Returns:
212
+ List of *n* sample instances.
213
+
214
+ Examples:
215
+ >>> @atdata.packable
216
+ ... class Point:
217
+ ... x: float
218
+ ... y: float
219
+ ... label: str
220
+ >>> points = make_samples(Point, n=5, seed=42)
221
+ >>> len(points)
222
+ 5
223
+ """
224
+ rng = np.random.default_rng(seed)
225
+ result: list[ST] = []
226
+
227
+ for i in range(n):
228
+ kwargs: dict[str, Any] = {}
229
+ for field in dc_fields(sample_type):
230
+ type_str = str(field.type)
231
+ fname = field.name
232
+
233
+ if field.type is str or type_str == "str":
234
+ kwargs[fname] = f"{fname}_{i}"
235
+ elif field.type is int or type_str == "int":
236
+ kwargs[fname] = i
237
+ elif field.type is float or type_str == "float":
238
+ kwargs[fname] = float(rng.random())
239
+ elif field.type is bool or type_str == "bool":
240
+ kwargs[fname] = i % 2 == 0
241
+ elif field.type is bytes or type_str == "bytes":
242
+ kwargs[fname] = rng.bytes(16)
243
+ elif "NDArray" in type_str or "ndarray" in type_str.lower():
244
+ kwargs[fname] = rng.standard_normal((4, 4)).astype(np.float32)
245
+ elif "list" in type_str.lower():
246
+ kwargs[fname] = [f"{fname}_{i}_{j}" for j in range(3)]
247
+ elif "None" in type_str:
248
+ # Optional field — leave at default
249
+ if field.default is not field.default_factory: # type: ignore[attr-defined]
250
+ continue
251
+ else:
252
+ kwargs[fname] = f"{fname}_{i}"
253
+
254
+ result.append(sample_type(**kwargs))
255
+
256
+ return result
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # Mock Index
261
+ # ---------------------------------------------------------------------------
262
+
263
+
264
+ def mock_index(path: Path | None = None, **kwargs: Any) -> Index:
265
+ """Create an in-memory SQLite-backed ``Index`` for testing.
266
+
267
+ No Redis or external services required.
268
+
269
+ Args:
270
+ path: Directory for the SQLite database file. If ``None``, uses
271
+ a temporary directory.
272
+ **kwargs: Additional keyword arguments passed to ``Index()``.
273
+
274
+ Returns:
275
+ An ``Index`` instance backed by a temporary SQLite database.
276
+
277
+ Examples:
278
+ >>> index = mock_index(tmp_path)
279
+ >>> ref = index.publish_schema(MyType, version="1.0.0")
280
+ """
281
+ if path is None:
282
+ path = Path(tempfile.mkdtemp())
283
+ db_path = path / "test_index.db"
284
+ provider = SqliteProvider(str(db_path))
285
+ return Index(provider=provider, atmosphere=None, **kwargs)
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # Pytest plugin (fixtures auto-discovered when atdata is installed)
290
+ # ---------------------------------------------------------------------------
291
+
292
+ try:
293
+ import pytest
294
+
295
+ @pytest.fixture
296
+ def mock_atmosphere():
297
+ """Provide a fresh ``MockAtmosphereClient`` for each test."""
298
+ client = MockAtmosphereClient()
299
+ client.login("test.mock.social", "test-password")
300
+ yield client
301
+ client.reset()
302
+
303
+ @pytest.fixture
304
+ def tmp_dataset(tmp_path: Path):
305
+ """Provide a small ``Dataset[SharedBasicSample]`` with 10 samples.
306
+
307
+ Uses ``SharedBasicSample`` (name: str, value: int) from the test suite.
308
+ """
309
+
310
+ @atdata.packable
311
+ class _TmpSample:
312
+ name: str
313
+ value: int
314
+
315
+ samples = [_TmpSample(name=f"s{i}", value=i) for i in range(10)]
316
+ return make_dataset(tmp_path, samples, sample_type=_TmpSample)
317
+
318
+ @pytest.fixture
319
+ def tmp_index(tmp_path: Path):
320
+ """Provide a fresh SQLite-backed ``Index`` for each test."""
321
+ return mock_index(tmp_path)
322
+
323
+ except ImportError:
324
+ # pytest not installed — skip fixture registration
325
+ _no_pytest = True
326
+
327
+
328
+ # ---------------------------------------------------------------------------
329
+ # Public API
330
+ # ---------------------------------------------------------------------------
331
+
332
+ __all__ = [
333
+ "MockAtmosphereClient",
334
+ "make_dataset",
335
+ "make_samples",
336
+ "mock_index",
337
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atdata
3
- Version: 0.2.3b1
3
+ Version: 0.3.0b1
4
4
  Summary: A loose federation of distributed, typed datasets
5
5
  Author-email: Maxine Levesque <hello@maxine.science>, "Maxine @ Forecast Bio" <maxine@forecast.bio>
6
6
  License-File: LICENSE
@@ -20,9 +20,12 @@ Requires-Dist: requests>=2.32.5
20
20
  Requires-Dist: s3fs>=2025.12.0
21
21
  Requires-Dist: schemamodels>=0.9.1
22
22
  Requires-Dist: tqdm>=4.67.1
23
+ Requires-Dist: typer>=0.21.1
23
24
  Requires-Dist: webdataset>=1.0.2
24
25
  Provides-Extra: atmosphere
25
26
  Requires-Dist: atproto>=0.0.55; extra == 'atmosphere'
27
+ Provides-Extra: postgres
28
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
26
29
  Description-Content-Type: text/markdown
27
30
 
28
31
  # atdata
@@ -0,0 +1,54 @@
1
+ atdata/.gitignore,sha256=ROcLwaiURIGOAYGFpakac_WEeKS4hH4HxJuscfDGo2s,10
2
+ atdata/__init__.py,sha256=TXEvc6R_VQ4zzjHr9OIqi_hhwfc6OqerVWajtDRwJTA,3249
3
+ atdata/_cid.py,sha256=6wLV_dcQJy5Eb-wld7_h7Kcp7QoVixIqUDIIoSwpQms,3992
4
+ atdata/_exceptions.py,sha256=a2IzmTJqLyIgQ0RkW3KhaaaSDdQJGLoXzuHfEX0DfB4,5390
5
+ atdata/_helpers.py,sha256=e-1lChOY9jvihs5Q04Tp5Rp9oKIoVf5N4wuuwv0Hl64,2224
6
+ atdata/_hf_api.py,sha256=ThNoVDIztbJCLtIKSCol4Tm7Lf1TqbU4E-fFsKt5FaA,24318
7
+ atdata/_logging.py,sha256=Wj9MTe9xCP_awiQYfa7219Fb443Rkwu_jIqM-m2u2GA,2225
8
+ atdata/_protocols.py,sha256=LYeM0eIn5nQ56DssdGFd-CvJFXh9C3YR2GLOSFxg1Kw,14479
9
+ atdata/_schema_codec.py,sha256=OpRs8bfCPGCEJsBKlp8rfndBldco3jwQQ7k9npN-EPQ,14395
10
+ atdata/_sources.py,sha256=A7HMkS_dqN5Sx7rG1nZsO1Laxozt3C65_P2Hiv41VXk,16624
11
+ atdata/_stub_manager.py,sha256=Heh0HAYjVjnkUQcPWAEOrkkEKN3Mi9vTJPA-ZRseFw8,19141
12
+ atdata/_type_utils.py,sha256=mhD0ywXgIEpVaAWpGVKGneUOh0cmI96zKz6g2O1bdg0,3762
13
+ atdata/dataset.py,sha256=ogclAFyeDh8ZGvgiDViiDn_sbkh94rpXgR-jtwFpi6M,41123
14
+ atdata/lens.py,sha256=s6Ebeacth4cF_yCmnH4MuZ6TwQJmUmrFZ3eIh1jQSZI,9914
15
+ atdata/promote.py,sha256=W8I_OCTStKY_Mu1OJvtVyJrWqmXjRQaTQLJ_Id-nBmA,6274
16
+ atdata/repository.py,sha256=5ZlYOw5VQdkuN0rntfw26CA_O_YsZLYMSGaRPPPn9Gc,10205
17
+ atdata/testing.py,sha256=OUPXn8CBuxNVyhj-lgIiw_YkTNrewdEkgFyfwogLH3E,10858
18
+ atdata/atmosphere/__init__.py,sha256=g8larX6DIA4AVFgt-g3vm6Ijsb4vU3YOAFPNCDMd-sk,10064
19
+ atdata/atmosphere/_types.py,sha256=MRhXnmAuQLJPdoq1skrBGXCsaQYdtKG_nA3YlSjwJXY,9595
20
+ atdata/atmosphere/client.py,sha256=acw82w3_cxWbWDtIRvH1VDHGJSroGqhSenFFostXTXo,16210
21
+ atdata/atmosphere/lens.py,sha256=EnrddTD-SAnyxU0XF__QkePLUhb4lPwfFLaseL_STDc,9260
22
+ atdata/atmosphere/records.py,sha256=Vkc7K0XFozwXVaDgXP706Z1-De1C95ZPtra1EQE3MGQ,15860
23
+ atdata/atmosphere/schema.py,sha256=xvIyJbzbZrcAi7TOWFQXXfgqAyuhb5Yqttf9dSy9x50,7758
24
+ atdata/atmosphere/store.py,sha256=NR4tGS9u3_ogvnyyOHDVF0tRKChruj_NE9Df4qrZiDU,6324
25
+ atdata/cli/__init__.py,sha256=3ZtMcYmx78L_8LqzPHaLlEOlFXl_uHBJtffSL6UB-kE,5834
26
+ atdata/cli/diagnose.py,sha256=Det9ozOvxXKd8Abu-xEsMjaXR34H_cuSX9MJJIlhnsA,5483
27
+ atdata/cli/inspect.py,sha256=YoJkWeXj1cHcOuDH9Z3btWMhiFNOG6JG3x-tScee-84,2019
28
+ atdata/cli/local.py,sha256=dYosOuVmp_PIM8re4DFsm00uspUJftTOZ0TBBNlTqAM,8105
29
+ atdata/cli/preview.py,sha256=n90Eqj920JEi7nBl4NnbmEhZxKLzHoR2CfNI9lkbGuc,1735
30
+ atdata/cli/schema.py,sha256=JwbQOBOPcvDTF6QgptuqZF5VEEMHrjxlj959q6E1DQY,2882
31
+ atdata/local/__init__.py,sha256=ZBJspq-dKOYZESUfxnQ0vIqpyBXGo2Fy116RvfFtDgs,1872
32
+ atdata/local/_entry.py,sha256=Cea1uAbURlBVn3vlrcLD5mtwIaqYoHjgb1GVsBqrszw,5264
33
+ atdata/local/_index.py,sha256=lCl0JAmHxmhz9lFlMiMvO_TApYARK3dyiBAJTVUaYO4,34305
34
+ atdata/local/_repo_legacy.py,sha256=7njQTp9HReyWNcIOyYG_Zg90nhEI8KRVCLI0ANnoUpY,7541
35
+ atdata/local/_s3.py,sha256=7KuIIZgqaiaNjULlKC3QPbLkKaj7rRVSYJt5zU_OEXI,12809
36
+ atdata/local/_schema.py,sha256=myCZ7eTJ0hKLQuF5qBvjzttyenKSqXd3YDdamFdRw1E,11634
37
+ atdata/manifest/__init__.py,sha256=GB7V8OXLDAIuUX4JSBVL7_UV9CFALMiMfoeZvoNkug0,1309
38
+ atdata/manifest/_aggregates.py,sha256=yabfBJtdVqOBWbkNqg9gp0BgpG62wi3QsGYlHgAAcFk,4198
39
+ atdata/manifest/_builder.py,sha256=abU5cnRnIJZ_qf_BKr6-fMYU1HaJWkBVgmGL0WzRRzU,5252
40
+ atdata/manifest/_fields.py,sha256=4Mm31HGb3qRwvp5Kn9Pp-4dX0tlNrQHxEDZJnBzFpos,5003
41
+ atdata/manifest/_manifest.py,sha256=hPcURPjjPS5r3H_veVshwT6Qw-lC9eQll84Fos1xrCE,4852
42
+ atdata/manifest/_query.py,sha256=25_0i60jWQhuMXsuuVLvSPS8v9pejTi5Bik99e2L99A,4641
43
+ atdata/manifest/_writer.py,sha256=_g2YZof-GnN3nVLpAAE5fDmsV9GzlPYf2oa1BWYswxI,2206
44
+ atdata/providers/__init__.py,sha256=LDziQc6Dc0QCfJcr_sDDqxArstDB0dSZHGgXCIIXbzo,848
45
+ atdata/providers/_base.py,sha256=3ICfgo2AWrRyAHrH8AJM4F0ssDBVD_K5iZAuQcUFLGA,4402
46
+ atdata/providers/_factory.py,sha256=9clKbch3gxqJWufY-HIbbfaBstzoq1LOcMrJOKHTPhw,2039
47
+ atdata/providers/_postgres.py,sha256=BpbjiD9jifLO74aWttSq4JgVoecAypCoYzO_7DdAZuQ,7402
48
+ atdata/providers/_redis.py,sha256=VljNKSsLAX5H0NoOuqkuLQeQAFuuettq1MnpBJgnqhw,6054
49
+ atdata/providers/_sqlite.py,sha256=urZWl7ErgKJJ1uBpnmmdkz7uWnrKGkJDIaTvAdyN7Uo,6486
50
+ atdata-0.3.0b1.dist-info/METADATA,sha256=zxCV6U6eMrtYZII4hBIrY7tZ9hs4ntTdAwPRhg_HISk,7410
51
+ atdata-0.3.0b1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
52
+ atdata-0.3.0b1.dist-info/entry_points.txt,sha256=6-iQr1veSTq-ac94bLyfcyGHprrZWevPEd12BWX37tQ,39
53
+ atdata-0.3.0b1.dist-info/licenses/LICENSE,sha256=Pz2eACSxkhsGfW9_iN60pgy-enjnbGTj8df8O3ebnQQ,16726
54
+ atdata-0.3.0b1.dist-info/RECORD,,