mpcq 0.2.3__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.
mpcq/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .client import MPCObservationsClient
2
+ from .observation import Observation, ObservationStatus
3
+ from .version import __version__
4
+
5
+ __all__ = ["MPCObservationsClient", "Observation", "ObservationStatus", "__version__"]
mpcq/client.py ADDED
@@ -0,0 +1,221 @@
1
+ import json
2
+ import logging
3
+ from typing import Iterator, Optional
4
+
5
+ import astropy.time
6
+ import google.cloud.secretmanager
7
+ import sqlalchemy as sq
8
+ from google.cloud.sql.connector import Connector
9
+
10
+ from .observation import Observation, ObservationStatus
11
+ from .submission import Submission
12
+
13
+ log = logging.getLogger("mpcq.client")
14
+
15
+ _DB_DRIVER = "pg8000"
16
+
17
+
18
+ class MPCObservationsClient:
19
+ def __init__(self, dbconn: sq.engine.Connection):
20
+ self._dbconn = dbconn
21
+
22
+ @classmethod
23
+ def connect(cls, engine: sq.engine.Engine):
24
+ return cls(dbconn=engine.connect())
25
+
26
+ @classmethod
27
+ def connect_using_gcloud(
28
+ cls,
29
+ cloudsql_connection_name: str = "moeyens-thor-dev:us-west1:mpc-replica",
30
+ credentials_uri: str = "projects/moeyens-thor-dev/secrets/mpc-replica-readonly-credentials/versions/latest", # noqa: E501
31
+ ):
32
+ log.info("loading database credentials")
33
+ client = google.cloud.secretmanager.SecretManagerServiceClient()
34
+ secret = client.access_secret_version(name=credentials_uri)
35
+ creds = json.loads(secret.payload.data)
36
+ log.info("database credentials loaded successfully")
37
+
38
+ connector = Connector()
39
+
40
+ def make_connection():
41
+ conn = connector.connect(
42
+ cloudsql_connection_name,
43
+ _DB_DRIVER,
44
+ user=creds["username"],
45
+ password=creds["password"],
46
+ db="mpc_sbn",
47
+ )
48
+ return conn
49
+
50
+ engine = sq.create_engine(
51
+ f"postgresql+{_DB_DRIVER}://", creator=make_connection
52
+ )
53
+ return cls.connect(engine)
54
+
55
+ def close(self):
56
+ self._dbconn.close()
57
+
58
+ def get_object_observations(
59
+ self,
60
+ object_provisional_designation: str,
61
+ obscode: Optional[str] = None,
62
+ filter_band: Optional[str] = None,
63
+ ) -> Iterator[Observation]:
64
+ stmt = self._observations_select_stmt(
65
+ object_provisional_designation, obscode, filter_band
66
+ )
67
+ result = self._dbconn.execute(stmt)
68
+ for r in result:
69
+ yield self._parse_obs_sbn_row(r)
70
+
71
+ def get_object_submissions(
72
+ self,
73
+ object_provisional_designation: str,
74
+ ) -> Iterator[Submission]:
75
+
76
+ stmt = self._submissions_select_stmt(object_provisional_designation)
77
+ result = self._dbconn.execute(stmt)
78
+ for r in result:
79
+ yield self._parse_submissions_sbn_row(r)
80
+
81
+ @staticmethod
82
+ def _parse_obs_sbn_row(row: sq.engine.Row) -> Observation:
83
+
84
+ if row.created_at is None:
85
+ created_at = None
86
+ else:
87
+ created_at = astropy.time.Time(row.created_at, scale="utc")
88
+
89
+ if row.updated_at is None:
90
+ updated_at = None
91
+ else:
92
+ updated_at = astropy.time.Time(row.updated_at, scale="utc")
93
+
94
+ return Observation(
95
+ mpc_id=row.id,
96
+ status=ObservationStatus._from_db_value(row.status),
97
+ obscode=row.stn,
98
+ filter_band=row.band,
99
+ unpacked_provisional_designation=row.provid,
100
+ timestamp=astropy.time.Time(row.obstime, scale="utc"),
101
+ ra=row.ra,
102
+ dec=row.dec,
103
+ mag=row.mag,
104
+ ra_rms=row.rmsra,
105
+ dec_rms=row.rmsdec,
106
+ mag_rms=row.rmsmag,
107
+ submission_id=row.submission_id,
108
+ created_at=created_at,
109
+ updated_at=updated_at,
110
+ )
111
+
112
+ @staticmethod
113
+ def _parse_submissions_sbn_row(row: sq.engine.Row) -> Submission:
114
+ return Submission(
115
+ id=row.submission_id,
116
+ num_observations=row.num_observations,
117
+ )
118
+
119
+ def _observations_select_stmt(
120
+ self,
121
+ provisional_designation: str,
122
+ obscode: Optional[str],
123
+ filter_band: Optional[str],
124
+ ) -> sq.sql.expression.Select:
125
+ """Construct a database select statement to fetch observations for
126
+ given object (named by provisional designation, eg "2022 AJ2").
127
+
128
+ obscode and filter_band can optionally be provided to limit the
129
+ result set.
130
+
131
+ """
132
+ log.info("loading observations for %s", provisional_designation)
133
+ stmt = (
134
+ sq.select(
135
+ sq.column("id"),
136
+ sq.column("stn"),
137
+ sq.column("status"),
138
+ sq.column("ra"),
139
+ sq.column("dec"),
140
+ sq.column("obstime"),
141
+ sq.column("provid"),
142
+ sq.column("rmsra"),
143
+ sq.column("rmsdec"),
144
+ sq.column("mag"),
145
+ sq.column("rmsmag"),
146
+ sq.column("band"),
147
+ sq.column("submission_id"),
148
+ sq.column("created_at"),
149
+ sq.column("updated_at"),
150
+ )
151
+ .select_from(sq.table("obs_sbn"))
152
+ .where(
153
+ sq.column("provid") == provisional_designation,
154
+ )
155
+ )
156
+ if obscode is not None:
157
+ stmt = stmt.where(sq.column("stn") == obscode)
158
+ if filter_band is not None:
159
+ stmt = stmt.where(sq.column("band") == filter_band)
160
+
161
+ return stmt
162
+
163
+ def _submissions_select_stmt(
164
+ self,
165
+ provisional_designation: str,
166
+ ) -> sq.sql.expression.Select:
167
+ """
168
+ Construct a database select statement to fetch submission IDs for
169
+ the given object (named by provisional designation, eg "2022 AJ2").
170
+
171
+ Parameters
172
+ ----------
173
+ provisional_designation : str
174
+ The provisional designation of the object to fetch submissions for.
175
+
176
+ Returns
177
+ -------
178
+ sq.sql.expression.Select
179
+ The select statement to fetch the submission IDs for the given object.
180
+ """
181
+ log.info("loading submissions for %s", provisional_designation)
182
+ stmt = (
183
+ sq.select(
184
+ sq.column("submission_id"),
185
+ sq.func.count(sq.column("submission_id")).label("num_observations"),
186
+ )
187
+ .select_from(sq.table("obs_sbn"))
188
+ .where(
189
+ sq.column("provid") == provisional_designation,
190
+ )
191
+ .group_by(sq.column("submission_id"))
192
+ )
193
+
194
+ return stmt
195
+
196
+ def object_counts_for_submission_id(
197
+ self, submission_id: str
198
+ ) -> list[tuple[str, int]]:
199
+ """
200
+ Queries for the number of observations for each object associated with a given submission ID.
201
+ """
202
+ log.info("loading object counts for submission ID %s", submission_id)
203
+ stmt = (
204
+ sq.select(
205
+ sq.column("provid"),
206
+ sq.func.count(sq.column("provid")).label("num_observations"),
207
+ )
208
+ .select_from(sq.table("obs_sbn"))
209
+ .where(
210
+ # HACK: the database currently only has an index for
211
+ # submission_block_id, even though we'd like to use
212
+ # submission_id.
213
+ sq.column("submission_block_id") == submission_id + "_01",
214
+ # We'd prefer this:
215
+ # sq.column("submission_id") == submission_id,
216
+ # but that index is still being built.
217
+ )
218
+ .group_by(sq.column("provid"))
219
+ )
220
+ result = self._dbconn.execute(stmt)
221
+ return [(row.provid, row.num_observations) for row in result]
mpcq/observation.py ADDED
@@ -0,0 +1,78 @@
1
+ import dataclasses
2
+ import decimal
3
+ from enum import Enum
4
+ from typing import Optional
5
+
6
+ import astropy.time
7
+
8
+
9
+ class ObservationStatus(Enum):
10
+ Published = 1
11
+ Pending = 2
12
+ IsolatedTrackletFile = 3
13
+
14
+ @classmethod
15
+ def _from_db_value(cls, val: str):
16
+ return {
17
+ "P": ObservationStatus.Published,
18
+ "p": ObservationStatus.Pending,
19
+ "I": ObservationStatus.IsolatedTrackletFile,
20
+ }[val]
21
+
22
+
23
+ @dataclasses.dataclass
24
+ class Observation:
25
+ """
26
+ Represents a single entry in the MPC Observations 'obs_sbn' table.
27
+ """
28
+
29
+ # The internal primary key, an opaque integer.
30
+ mpc_id: int
31
+
32
+ # See ObservationStatus.
33
+ status: ObservationStatus
34
+
35
+ # The three-character code which identifies the observatory which
36
+ # made the observation.
37
+ obscode: str
38
+
39
+ # The single-character code which identifies which filter was used
40
+ # when observing.
41
+ filter_band: str
42
+
43
+ # An identifier for the object being observed, for example '2022 AJ2'.
44
+ unpacked_provisional_designation: str
45
+
46
+ # The time of the observation. This is submitted to MPC, and does
47
+ # not have a well-defined meaning. In general, it ought to be the
48
+ # midpoint of the exposure that the observation came from, but
49
+ # sometimes it seems to be the start of the exposure. For
50
+ # observations which come from stacks of multiple exposures it is
51
+ # even less well-defined.
52
+ timestamp: astropy.time.Time
53
+
54
+ # Right ascension of the observation in the J2000 frame.
55
+ ra: decimal.Decimal
56
+ # Error in the right ascension.
57
+ ra_rms: Optional[decimal.Decimal]
58
+
59
+ # Declination of the observation in the J2000 frame.
60
+ dec: decimal.Decimal
61
+ # Error in the declination.
62
+ dec_rms: Optional[decimal.Decimal]
63
+
64
+ # Observed magnitude of the object.
65
+ mag: Optional[decimal.Decimal]
66
+ # Error in the magnitude estimate.
67
+ mag_rms: Optional[decimal.Decimal]
68
+
69
+ # Submission Properties
70
+ # MPC-assigned ID for the submission
71
+ submission_id: str
72
+
73
+ # Time when the observation was first submitted to MPC and ingested
74
+ # Apparently this can be None... maybe for older observations???
75
+ created_at: Optional[astropy.time.Time]
76
+ # Time when the last update to this observations was made, None if
77
+ # never updated beyond initial creation
78
+ updated_at: Optional[astropy.time.Time]
mpcq/submission.py ADDED
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from astropy.time import Time
4
+
5
+
6
+ def timestamp_from_submission_id(submission_id: str):
7
+ timestamp_isot = submission_id.split("_")[0]
8
+ return Time(timestamp_isot, format="isot")
9
+
10
+
11
+ @dataclass
12
+ class Submission:
13
+ # MPC-assigned submission ID (same as submission_id in Observation)
14
+ id: str
15
+
16
+ # Added-value (number of observations in submission)
17
+ num_observations: int
18
+
19
+ # Added-value (extracted from submission ID)
20
+ timestamp: Time = field(init=False)
21
+
22
+ def __post_init__(self):
23
+ self.timestamp = timestamp_from_submission_id(self.id)
mpcq/tests/__init__.py ADDED
File without changes
@@ -0,0 +1,58 @@
1
+ import os
2
+
3
+ import pytest
4
+
5
+ from ..client import MPCObservationsClient
6
+
7
+
8
+ @pytest.mark.integration
9
+ @pytest.mark.skipif(
10
+ "MPCQ_INTEGRATION_TESTS" not in os.environ,
11
+ reason="MPCQ_INTEGRATION_TESTS must be set",
12
+ )
13
+ class TestIntegration:
14
+ def test_connection(self):
15
+ # Test that a connection can be established
16
+ MPCObservationsClient.connect_using_gcloud()
17
+
18
+ def test_get_object_observations(self, mpc_client):
19
+ observations = list(mpc_client.get_object_observations("2022 AJ2"))
20
+ assert len(observations) >= 10
21
+ for o in observations:
22
+ assert o.unpacked_provisional_designation == "2022 AJ2"
23
+
24
+ def test_get_object_observations_filter_by_obscode(self, mpc_client):
25
+ observations = mpc_client.get_object_observations("2022 AJ2", obscode="I52")
26
+ observations = list(observations)
27
+ assert len(observations) > 0
28
+ for o in observations:
29
+ assert o.unpacked_provisional_designation == "2022 AJ2"
30
+ assert o.obscode == "I52"
31
+
32
+ def test_get_object_observations_filter_by_filter_band(self, mpc_client):
33
+ observations = mpc_client.get_object_observations("2022 AJ2", filter_band="G")
34
+ observations = list(observations)
35
+ assert len(observations) > 0
36
+ for o in observations:
37
+ assert o.unpacked_provisional_designation == "2022 AJ2"
38
+ assert o.filter_band == "G"
39
+
40
+ def test_get_object_submissions(self, mpc_client):
41
+ submissions = list(mpc_client.get_object_submissions("2022 AJ2"))
42
+ assert len(submissions) >= 1
43
+ for s in submissions:
44
+ assert s.num_observations >= 1
45
+
46
+ observations = mpc_client.get_object_observations("2022 AJ2")
47
+ observations = list(observations)
48
+
49
+ num_observations_from_submissions = sum(s.num_observations for s in submissions)
50
+ num_observations = len(observations)
51
+ assert num_observations_from_submissions == num_observations
52
+
53
+
54
+ @pytest.fixture
55
+ def mpc_client():
56
+ client = MPCObservationsClient.connect_using_gcloud()
57
+ yield client
58
+ client.close()
@@ -0,0 +1,11 @@
1
+ from astropy.time import Time
2
+
3
+ from mpcq.submission import timestamp_from_submission_id
4
+
5
+
6
+ def test_timestamp_from_submission_id():
7
+
8
+ submission_id = "2020-10-23T06:39:01.200_0000DvZH"
9
+ desired_timestamp = Time("2020-10-23T06:39:01.200", format="isot")
10
+
11
+ assert timestamp_from_submission_id(submission_id) == desired_timestamp
@@ -0,0 +1,69 @@
1
+ import decimal
2
+
3
+ import pandas as pd
4
+ from astropy.time import Time
5
+
6
+ from mpcq.observation import Observation, ObservationStatus
7
+ from mpcq.submission import Submission
8
+ from mpcq.utils import observations_to_dataframe, submissions_to_dataframe
9
+
10
+
11
+ def test_observations_to_dataframe():
12
+
13
+ observation = Observation(
14
+ mpc_id=1000,
15
+ status=ObservationStatus.Published,
16
+ obscode="I41",
17
+ filter_band="R",
18
+ unpacked_provisional_designation="2022 AJ2",
19
+ timestamp=Time("2021-01-01T00:00:00.000", format="isot"),
20
+ ra=decimal.Decimal(1.0),
21
+ ra_rms=decimal.Decimal(0.001),
22
+ dec=decimal.Decimal(-10.0),
23
+ dec_rms=decimal.Decimal(0.004),
24
+ mag=decimal.Decimal(20.0),
25
+ mag_rms=decimal.Decimal(0.1),
26
+ submission_id="2020-10-23T06:39:01.200_0000DvZH",
27
+ created_at=Time("2020-10-23T06:39:01.200", format="isot"),
28
+ updated_at=Time("2020-10-23T06:39:01.200", format="isot"),
29
+ )
30
+
31
+ desired_df = pd.DataFrame(
32
+ {
33
+ "mpc_id": [1000],
34
+ "status": ["Published"],
35
+ "obscode": ["I41"],
36
+ "filter_band": ["R"],
37
+ "unpacked_provisional_designation": ["2022 AJ2"],
38
+ "timestamp": [Time("2021-01-01T00:00:00.000", format="isot").datetime],
39
+ "ra": [decimal.Decimal(1.0)],
40
+ "ra_rms": [decimal.Decimal(0.001)],
41
+ "dec": [decimal.Decimal(-10.0)],
42
+ "dec_rms": [decimal.Decimal(0.004)],
43
+ "mag": [decimal.Decimal(20.0)],
44
+ "mag_rms": [decimal.Decimal(0.1)],
45
+ "submission_id": ["2020-10-23T06:39:01.200_0000DvZH"],
46
+ "created_at": [Time("2020-10-23T06:39:01.200", format="isot").datetime],
47
+ "updated_at": [Time("2020-10-23T06:39:01.200", format="isot").datetime],
48
+ }
49
+ )
50
+
51
+ pd.testing.assert_frame_equal(observations_to_dataframe([observation]), desired_df)
52
+
53
+
54
+ def test_submissions_to_dataframe():
55
+
56
+ submission = Submission(
57
+ id="2020-10-23T06:39:01.200_0000DvZH",
58
+ num_observations=5,
59
+ )
60
+
61
+ desired_df = pd.DataFrame(
62
+ {
63
+ "id": ["2020-10-23T06:39:01.200_0000DvZH"],
64
+ "num_observations": [5],
65
+ "timestamp": [Time("2020-10-23T06:39:01.200", format="isot").datetime],
66
+ }
67
+ )
68
+
69
+ pd.testing.assert_frame_equal(submissions_to_dataframe([submission]), desired_df)
mpcq/utils.py ADDED
@@ -0,0 +1,57 @@
1
+ from dataclasses import asdict
2
+ from typing import List
3
+
4
+ import pandas as pd
5
+
6
+ from .observation import Observation
7
+ from .submission import Submission
8
+
9
+
10
+ def observations_to_dataframe(observations: List[Observation]) -> pd.DataFrame:
11
+ """
12
+ Convert a list of Observation objects to a pandas DataFrame.
13
+
14
+ Parameters
15
+ ----------
16
+ observations : List[Observation]
17
+ The observations to convert.
18
+
19
+ Returns
20
+ -------
21
+ observations : `~pd.DataFrame`
22
+ """
23
+ data = [asdict(obs) for obs in observations]
24
+
25
+ # Convert ObservationStatus objects to strings.
26
+ # Convert Time objects to datetime objects.
27
+ for row in data:
28
+ row["status"] = row["status"].name
29
+ row["timestamp"] = row["timestamp"].datetime
30
+ if row["created_at"] is not None:
31
+ row["created_at"] = row["created_at"].datetime
32
+ if row["updated_at"] is not None:
33
+ row["updated_at"] = row["updated_at"].datetime
34
+
35
+ return pd.DataFrame(data)
36
+
37
+
38
+ def submissions_to_dataframe(submissions: List[Submission]) -> pd.DataFrame:
39
+ """
40
+ Convert a list of Submission objects to a pandas DataFrame.
41
+
42
+ Parameters
43
+ ----------
44
+ submissions : List[Submission]
45
+ The submissions to convert.
46
+
47
+ Returns
48
+ -------
49
+ submissions : `~pd.DataFrame`
50
+ """
51
+ data = [asdict(sub) for sub in submissions]
52
+
53
+ # Convert Time objects to datetime objects.
54
+ for row in data:
55
+ row["timestamp"] = row["timestamp"].datetime
56
+
57
+ return pd.DataFrame(data)
mpcq/version.py ADDED
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("mpcq")
5
+ except PackageNotFoundError:
6
+ # package is not installed
7
+ __version__ = "unknown"
@@ -0,0 +1,76 @@
1
+ Metadata-Version: 2.1
2
+ Name: mpcq
3
+ Version: 0.2.3
4
+ Requires-Python: >=3.9
5
+ Description-Content-Type: text/markdown
6
+ Requires-Dist: sqlalchemy (<2)
7
+ Requires-Dist: pandas
8
+ Requires-Dist: astropy
9
+ Requires-Dist: google-cloud-secret-manager
10
+ Requires-Dist: cloud-sql-python-connector[pg8000]
11
+ Requires-Dist: pg8000
12
+ Provides-Extra: tests
13
+ Requires-Dist: pre-commit ; extra == 'tests'
14
+ Requires-Dist: pytest ; extra == 'tests'
15
+
16
+ # mpcq
17
+ #### A Python package by the Asteroid Institute, a program of the B612 Foundation
18
+
19
+ This is a client library for interacting with an MPC observations database.
20
+
21
+ ## Usage
22
+
23
+ To connect to the Asteroid Institute's clone of the Small Bodies Node MPC database:
24
+ ```python
25
+ from mpcq.client import MPCObservationsClient
26
+
27
+ client = MPCObservationsClient.connect_using_gcloud()
28
+ ```
29
+
30
+ With a client initialized, you can get all observations of a particular object using its provisional designation:
31
+ ```python
32
+ observations = client.get_object_observations("2013 RR165")
33
+ observations = list(observations)
34
+ ```
35
+
36
+ These can be converted into a dataframe as follows:
37
+
38
+ ```python
39
+ from mpcq.utils import observations_to_dataframe
40
+
41
+ observations_df = observations_to_dataframe(observations)
42
+ print(observations_df.head(5))
43
+ ```
44
+ ```
45
+ mpc_id status obscode filter_band unpacked_provisional_designation timestamp ra ra_rms dec dec_rms mag mag_rms submission_id created_at updated_at
46
+ 0 174511900 Published F51 w 2013 RR165 2011-01-30 11:15:26 123.884679 None 19.820047 None 22.2 None 2011-04-12T00:57:19.000_00005L9j 2017-07-10 00:00:00.000000 2022-06-15 17:17:33.421485
47
+ 1 174511901 Published F51 w 2013 RR165 2011-01-30 11:37:23 123.880767 None 19.820603 None 22.4 None 2011-04-12T00:57:19.000_00005L9j 2017-07-10 00:00:00.000000 2022-06-15 17:17:33.427512
48
+ 2 174511902 Published F51 w 2013 RR165 2011-01-30 12:22:35 123.872683 None 19.821694 None 22.3 None 2011-04-12T00:57:19.000_00005L9j 2017-07-10 00:00:00.000000 2022-06-15 17:17:33.431369
49
+ 3 394474985 Published W84 g 2013 RR165 2013-09-02 05:49:09 354.49745627 0.097 1.17373181 0.100 21.66 0.07 2022-05-23T23:16:35.633_0000EfpX 2022-05-23 23:18:28.963374 2022-06-15 17:17:33.434170
50
+ 4 175542203 Published F51 w 2013 RR165 2013-09-03 10:20:19 354.259229 None 1.099064 None 21.6 None 2013-09-04T00:37:51.000_00005cQy 2017-07-10 00:00:00.000000 2022-06-15 17:17:33.436820
51
+ ```
52
+
53
+ Getting the submission ID and the number of observations per submission of an object:
54
+ ```python
55
+ submissions = client.get_object_submissions("2013 RR165")
56
+ submissions = list(submissions)
57
+ ```
58
+
59
+ As before, these can be converted to a dataframe:
60
+ ```python
61
+ from mpcq.utils import submissions_to_dataframe
62
+
63
+ submissions_df = submissions_to_dataframe(submissions)
64
+ print(submissions_df)
65
+ ```
66
+ ```
67
+ id num_observations timestamp
68
+ 0 2017-07-05T20:57:57.001_00006dS8 3 2017-07-05 20:57:57.001
69
+ 1 2013-09-04T00:37:51.000_00005cQy 3 2013-09-04 00:37:51.000
70
+ 2 2017-09-13T19:53:09.000_0000CdSX 2 2017-09-13 19:53:09.000
71
+ 3 2022-05-23T23:16:35.633_0000EfpX 19 2022-05-23 23:16:35.633
72
+ 4 2013-09-12T11:43:25.001_00005cpl 4 2013-09-12 11:43:25.001
73
+ 5 2011-04-12T00:57:19.000_00005L9j 3 2011-04-12 00:57:19.000
74
+ 6 2016-04-03T19:54:29.000_00006IpM 3 2016-04-03 19:54:29.000
75
+ 7 2015-01-16T23:14:02.000_00005wg5 3 2015-01-16 23:14:02.000
76
+ ```
@@ -0,0 +1,14 @@
1
+ mpcq/__init__.py,sha256=w7f2XUymlsZSzrKSmez08cBBM3HF2xznNnDfKevVRe0,219
2
+ mpcq/client.py,sha256=wUEWu2oJm5hVI7LwA5lVKWMVk0jpTDnIngaUL380o-U,7335
3
+ mpcq/observation.py,sha256=n1eQ8hFWh1HsFhejlX2Wqb-cqMVkQWAqWs3FrmHSm-A,2354
4
+ mpcq/submission.py,sha256=wkxU-PavFnHOhD14UwPWXFsx8ScK7U6N4EbECG3cw8M,606
5
+ mpcq/utils.py,sha256=IHFUUaWBGo1xXGJbrfxdFuWh-luNLaIpzKvvwHLyQuM,1499
6
+ mpcq/version.py,sha256=gfIUsZ8--RIaUj-9NvEiDCoybvDLeoOILCvAnJ9F8Yg,189
7
+ mpcq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ mpcq/tests/test_integration.py,sha256=avnBZXk1fEZP1kt2KbOt0sZ00GQDyUkC29s1M7OyYo4,2093
9
+ mpcq/tests/test_submission.py,sha256=RbwnihJ8UAihd4FZbErW7Rpq0jzvI1gru3mVSvsb0Eo,335
10
+ mpcq/tests/test_utils.py,sha256=gJUtA9NOJn130OhBEe18feyljEU8fajiolYR7q9jUuk,2374
11
+ mpcq-0.2.3.dist-info/METADATA,sha256=8vaSYGIp2Of4zJvgEunhiwImiAkmEuxnhX7W-4QR53Y,3225
12
+ mpcq-0.2.3.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
13
+ mpcq-0.2.3.dist-info/top_level.txt,sha256=vldAS3-4ybSuV_Y3YQJbdEQ16MmwUrhvCt_5uU-AUPg,5
14
+ mpcq-0.2.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.40.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ mpcq