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 +5 -0
- mpcq/client.py +221 -0
- mpcq/observation.py +78 -0
- mpcq/submission.py +23 -0
- mpcq/tests/__init__.py +0 -0
- mpcq/tests/test_integration.py +58 -0
- mpcq/tests/test_submission.py +11 -0
- mpcq/tests/test_utils.py +69 -0
- mpcq/utils.py +57 -0
- mpcq/version.py +7 -0
- mpcq-0.2.3.dist-info/METADATA +76 -0
- mpcq-0.2.3.dist-info/RECORD +14 -0
- mpcq-0.2.3.dist-info/WHEEL +5 -0
- mpcq-0.2.3.dist-info/top_level.txt +1 -0
mpcq/__init__.py
ADDED
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
|
mpcq/tests/test_utils.py
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
mpcq
|