Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use new HTTP API to access the database #25

Merged
merged 26 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
00b749c
Added barebones AlerceDirect class, for making direct requests to the
ASHuenchuleo Nov 18, 2022
79a4d06
Changed the config parameter for the db api along with the README
ASHuenchuleo Nov 18, 2022
d88f0d3
Result class is now divided in csv result and json result, modified code
ASHuenchuleo Nov 21, 2022
40fcbfb
Implemented the direct requests class, and added support for
ASHuenchuleo Nov 21, 2022
6b8ad29
ci: github actions schedule deleted + yml notation
Nov 24, 2022
ecf033f
style: some spaces deleted
Nov 24, 2022
7e0f5b6
Result now inherits from abc.ABC
ASHuenchuleo Nov 25, 2022
f3ab863
Removed print statements
ASHuenchuleo Nov 25, 2022
4b3e0d3
Merge branch 'feature/use-api' of github.com:alercebroker/alerce_clie…
ASHuenchuleo Nov 25, 2022
4f86766
Formatting on util.py
ASHuenchuleo Nov 25, 2022
42baf1d
Fixed error on the object intiialization section of the README
ASHuenchuleo Nov 25, 2022
359682f
More specific exception handling in handle_error
ASHuenchuleo Nov 25, 2022
d7f3ba9
Added initialization tests for Alerce
ASHuenchuleo Nov 25, 2022
655eabb
Black formatting
ASHuenchuleo Nov 25, 2022
8e9bf0c
clean: Removed leftover print
pcastelln Nov 25, 2022
7717d82
result now raises error if format is unknown
pcastelln Nov 25, 2022
dda6c77
refactored: Cleaned up Alerce class and unused method in AlerceSearch
pcastelln Nov 25, 2022
466f865
refactored: removed unused imports
pcastelln Nov 25, 2022
edc59d0
conf: Coverage now ignores abstract methods
pcastelln Nov 25, 2022
2306766
style: Applied black formatting
pcastelln Nov 25, 2022
f05cb51
Update tests.yml
ASHuenchuleo Dec 2, 2022
7eb202a
Updated README
ASHuenchuleo Dec 5, 2022
876c5b7
Merge branch 'feature/use-api' of github.com:alercebroker/alerce_clie…
ASHuenchuleo Dec 5, 2022
0f4691a
Client now handles the response content correctly.
ASHuenchuleo Dec 6, 2022
5bcd9ed
Changed default DB API to the development API.
ASHuenchuleo Dec 12, 2022
14a8df9
Merge branch 'main' into feature/use-api
ASHuenchuleo Dec 13, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[report]
exclude_lines:
pragma: no cover
@(?:abc\.)?abstractmethod
10 changes: 4 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,15 @@ name: Tests

on:
push:
branches: [ main ]
branches:
- main
dirodriguezm marked this conversation as resolved.
Show resolved Hide resolved
pull_request:
branches: [ main ]
schedule:
- cron: '0 9 * * MON,WED,FRI'
branches:
- main

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
Expand Down
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
.python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
Expand Down Expand Up @@ -138,4 +138,8 @@ dmypy.json

# Cython debug symbols
cython_debug/
.idea/
.idea/

#pyenv
Pipfile.lock
Pipfile
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,9 @@ At the client object initialization
You can pass parameters to the Alerce class constructor to set the
parameters for API connection.

For example using an API on localhost: .. code-block:: python

For example using the ZTF API on localhost:5000 and the DB API on localhost:5050
``` {.sourceCode .python}
alerce = Alerce(ZTF_API_URL="<http://localhost:5000>")
alerce = Alerce(ZTF_API_URL="<http://localhost:5000>", ZTF_DB_API_URL="<http://localhost:5050>")
```

From a dictionary object
Expand All @@ -70,6 +69,7 @@ You can pass parameters to the Alerce class from a dictionary object.
``` {.sourceCode .python}
my_config = {
"ZTF_API_URL": "http://localhost:5000"
"ZTF_DB_API_URL": "http://localhost:5050"
}
alerce = Alerce()
alerce.load_config_from_object(my_config)
Expand Down
20 changes: 3 additions & 17 deletions alerce/core.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from .search import AlerceSearch
from .crossmatch import AlerceXmatch
from .stamps import AlerceStamps
from .direct import AlerceDirect


class Alerce(AlerceSearch, AlerceXmatch, AlerceStamps):
class Alerce(AlerceSearch, AlerceXmatch, AlerceStamps, AlerceDirect):
pcastelln marked this conversation as resolved.
Show resolved Hide resolved
"""
The main client class that has all the methods for accessing the different services.

Expand All @@ -18,19 +19,4 @@ class Alerce(AlerceSearch, AlerceXmatch, AlerceStamps):
The url of the ZTF API
"""

def __init__(self, **kwargs):
super().__init__(**kwargs)

def load_config_from_file(self, path):
pass

def load_config_from_object(self, object):
"""
Sets configuration parameters from a dictionary object.

Parameters
------------
object : dict
The dictionary containing the config.
"""
super().load_config_from_object(object)
pass
36 changes: 36 additions & 0 deletions alerce/direct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from .utils import Client


class AlerceDirect(Client):

"""Handles direct request to the database using the available http API"""

def __init__(self, **kwargs):
default_config = {
"ZTF_DB_API_URL": "https://api.alerce.online/db/",
}
default_config.update(kwargs)
super().__init__(**default_config)

def __get_url(self):
return f"{self.config['ZTF_DB_API_URL']}"

def send_query(self, query, format="csv", index=None, sort=None):
"""Sends the query directly to the API, returning the byte reply directly

:query: query for the database
:format: Format to be returned
:index: index if format is pandas
:sort: sorting column if format is pandas
:returns: byte reply

"""
data = {"query": query}
q = self._request(
"POST",
self.__get_url(),
data=data,
result_format=format,
response_format="csv",
)
return q.result(index, sort)
23 changes: 16 additions & 7 deletions alerce/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
def handle_error(response):
import requests


def handle_error(response, response_format="json"):
# TODO: The direct API uses code 400 for user input error (bad requests, etc), what should be done here then?
codes = {-1: APIError, 400: ParseError, 404: ObjectNotFoundError}
try:
error = response.json().get("errors", {})
message = response.json().get("message")
except:
message = "Unknown API error."
error = "Unknown API error."
message = "Unknown API error."
error = "Unknown API error."
if response_format == "json":
try:
error = response.json().get("errors", {})
message = response.json().get("message")
except requests.exceptions.JSONDecodeError:
pass
elif response_format == "csv":
error = response.content.decode("utf-8")
message = response.content.decode("utf-8")
code = response.status_code
data = error

Expand Down
11 changes: 1 addition & 10 deletions alerce/search.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from .exceptions import FormatValidationError, ParseError, handle_error
from .utils import Result, Client
from .utils import Client


class AlerceSearch(Client):
Expand Down Expand Up @@ -30,14 +29,6 @@ def ztf_url(self):
def __get_url(self, resource, *args):
return self.ztf_url + self.config["ZTF_ROUTES"][resource] % args

def __validate_format(self, format):
format = format.lower()
if not format in self.allowed_formats:
raise FormatValidationError(
"Format '%s' not in %s" % (format, self.allowed_formats), code=500
)
return format

def query_objects(self, format="pandas", index=None, sort=None, **kwargs):
"""
Gets a list of objects filtered by specified parameters.
Expand Down
140 changes: 121 additions & 19 deletions alerce/utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,65 @@
from pandas import DataFrame
from pandas import DataFrame, read_csv
from io import StringIO
from astropy.table import Table
from .exceptions import handle_error, FormatValidationError
import requests
import abc


class Result:
def __init__(self, json_result, format="json"):
self.json_result = json_result
class Result(abc.ABC):
def __init__(self, format="json"):
self.format = format

@abc.abstractmethod
def to_pandas(self, index=None, sort=None):
"""Convert the result to a pandas dataframe

:index: index for the pandas dataframe
:sort: sorting column for the dataframe
:returns: the processed dataframe

"""
pass

@abc.abstractmethod
def to_votable(self):
pass

@abc.abstractmethod
def to_json(self):
pass

@abc.abstractmethod
def to_csv(self, index=None, sort=None):
pass

def result(self, index=None, sort=None):
"""Creates the result depending on the arguments and the expected format

:index: if format is pandas, this is the index column
:sort: if format is pandas, this is the sort column
:returns: Result in the indicated format

"""
if self.format == "pandas":
return self.to_pandas(index, sort)
elif self.format == "votable":
return self.to_votable()
elif self.format == "csv":
return self.to_csv()
elif self.format == "json":
return self.to_json()
raise ValueError(f"Unrecognized format '{self.format}'")


class ResultJson(Result):
"""Object that holds a json type result"""

def __init__(self, json_result, **kwargs):
self.json_result = json_result
super().__init__(**kwargs)

def to_pandas(self, index=None, sort=None):
dataframe = None
if isinstance(self.json_result, list):
dataframe = DataFrame(self.json_result)
else:
Expand All @@ -27,21 +76,64 @@ def to_votable(self):
def to_json(self):
return self.json_result

def result(self, index=None, sort=None):
if self.format == "json":
return self.to_json()
if self.format == "pandas":
return self.to_pandas(index, sort)
if self.format == "votable":
return self.to_votable()
def to_csv(self):
df = self.to_pandas()
return df.to_csv(index=False)


class ResultCsv(Result):

"""Object that holds a csv type result"""

def __init__(self, csv_result_byte, **kwargs):
self.csv_result = csv_result_byte.decode("utf-8")
self.data = [x.split(",") for x in self.csv_result.split("\n")[1:-1]]

self.columns = [x for x in self.csv_result.split("\n")[0].split(",")]
super().__init__(**kwargs)

def _rename_duplicates(self):
counts = {}
columns = self.columns.copy()
for i, column in enumerate(columns):
if column in counts:
counts[column] += 1
columns[i] = f"{column}_{counts[column] - 1}"
else:
counts[column] = 1
return columns

def to_pandas(self, index=None, sort=None):
dataframe = read_csv(StringIO(self.csv_result))
if sort:
dataframe.sort_values(sort, inplace=True)
if index:
dataframe.set_index(index, inplace=True)
return dataframe

def to_votable(self):
# TODO: Check if renaming the columns doesn't cause problems to the user
columns = self._rename_duplicates()
df = read_csv(StringIO(self.csv_result), names=columns, skiprows=1)
df = df.convert_dtypes()
table = Table.from_pandas(df)
return table

def to_json(self):
columns = self._rename_duplicates()
df = read_csv(StringIO(self.csv_result), names=columns, skiprows=1)
return df.to_json(orient="records")

def to_csv(self):
return self.csv_result


class Client:
def __init__(self, **kwargs):
self.session = requests.Session()
self.config = {}
self.config.update(kwargs)
self.allowed_formats = ["pandas", "votable", "json"]
self.allowed_formats = ["pandas", "votable", "json", "csv"]

def load_config_from_file(self, path):
pass
Expand All @@ -58,13 +150,23 @@ def _validate_format(self, format):
return format

def _request(
self, method, url, params=None, response_field=None, result_format="json"
self,
method,
url,
params=None,
data=None,
response_field=None,
result_format="json",
response_format="json",
):
result_format = self._validate_format(result_format)
resp = self.session.request(method, url, params=params)

resp = self.session.request(method, url, params=params, data=data)
if resp.status_code >= 400:
handle_error(resp)
if response_field and result_format != "json":
return Result(resp.json()[response_field], format=result_format)
return Result(resp.json(), format=result_format)
handle_error(resp, response_format)

if response_format == "csv":
return ResultCsv(resp, format=result_format)
if response_field and result_format != "json" and result_format != "csv":
return ResultJson(resp.json()[response_field], format=result_format)
return ResultJson(resp.json(), format=result_format)
Loading