Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' of github.com:cityofaustin/pypgrest
- Loading branch information
Showing
5 changed files
with
274 additions
and
122 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# This workflows will upload a Python Package using Twine when a release is created | ||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries | ||
|
||
name: Publish dev package to PyPI | ||
|
||
on: | ||
push: | ||
branches: [ "dev" ] | ||
|
||
jobs: | ||
deploy: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.x' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install setuptools wheel twine | ||
- name: Build and publish | ||
env: | ||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} | ||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} | ||
run: | | ||
# note the use of `pypi-dev` to flag our dev package deployment | ||
python setup.py pypi-dev sdist bdist_wheel | ||
twine upload dist/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# This workflows will upload a Python Package using Twine when a release is created | ||
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries | ||
|
||
name: Publish prod package to PyPI | ||
|
||
on: | ||
release: | ||
types: [created] | ||
|
||
jobs: | ||
deploy: | ||
|
||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Set up Python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: '3.x' | ||
- name: Install dependencies | ||
run: | | ||
python -m pip install --upgrade pip | ||
pip install setuptools wheel twine | ||
- name: Build and publish | ||
env: | ||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} | ||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} | ||
run: | | ||
python setup.py sdist bdist_wheel | ||
twine upload dist/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,109 +1,132 @@ | ||
""" | ||
A Python client for interacting with PostgREST APIs. | ||
""" | ||
from copy import deepcopy | ||
import json | ||
import math | ||
|
||
import requests | ||
|
||
|
||
class Postgrest(object): | ||
""" | ||
Class to interact with PostgREST. | ||
""" | ||
def __init__(self, url, auth=None): | ||
"""Class to interact with PostgREST""" | ||
|
||
self.auth = auth | ||
def __init__(self, url, token=None, headers=None): | ||
self.token = token | ||
self.url = url | ||
|
||
self.headers = { | ||
"Content-Type": "application/json", | ||
"Prefer": "return=representation", # return entire record json in response | ||
} | ||
|
||
if self.auth: | ||
self.headers["Authorization"] = f"Bearer {self.auth}" | ||
|
||
|
||
def insert(self, data=None): | ||
self.res = requests.post(self.url, headers=self.headers, json=data) | ||
self.res.raise_for_status() | ||
return self.res.json() | ||
|
||
|
||
def update(self, params=None, data=None): | ||
""" | ||
This method is dangerous! It is possible to delete and modify records | ||
en masse. Read the PostgREST docs. | ||
""" | ||
self.res = requests.patch(self.url, headers=self.headers, params=params, json=data) | ||
self.default_headers = {"Content-Type": "application/json"} | ||
if self.token: | ||
self.default_headers["Authorization"] = f"Bearer {self.token}" | ||
if headers: | ||
self.default_headers.update(headers) | ||
|
||
def _make_request(self, *, resource, method, headers, params=None, data=None): | ||
self.res = None | ||
url = f"{self.url}/{resource}" | ||
req = requests.Request( | ||
method, | ||
url, | ||
headers=headers, | ||
params=params, | ||
json=data, | ||
) | ||
prepped = req.prepare() | ||
session = requests.Session() | ||
self.res = session.send(prepped) | ||
self.res.raise_for_status() | ||
return self.res.json() | ||
|
||
|
||
def upsert(self, data=None): | ||
""" | ||
This method is dangerous! It is possible to delete and modify records | ||
en masse. Read the PostgREST docs. | ||
""" | ||
headers = deepcopy(self.headers) | ||
headers["Prefer"] += ", resolution=merge-duplicates" | ||
self.res = requests.post(self.url, headers=headers, json=data) | ||
self.res.raise_for_status() | ||
return self.res.json() | ||
|
||
|
||
def delete(self, params=None): | ||
""" | ||
This method is dangerous! It is possible to delete and modify records | ||
en masse. Read the PostgREST docs. | ||
""" | ||
try: | ||
return self.res.json() | ||
except json.JSONDecodeError: | ||
return self.res.text | ||
|
||
def _get_request_headers(self, headers): | ||
"""Update the instance's default request headers with any provided by the user | ||
when making a request""" | ||
request_headers = deepcopy(self.default_headers) | ||
if headers: | ||
request_headers.update(headers) | ||
return request_headers | ||
|
||
def insert(self, resource, data=None, headers=None): | ||
headers = self._get_request_headers(headers) | ||
return self._make_request( | ||
resource=resource, method="post", headers=headers, data=data | ||
) | ||
|
||
def update(self, resource, params=None, data=None, headers=None): | ||
"""This method is dangerous! It is possible to delete and modify records | ||
en masse. Read the PostgREST docs.""" | ||
headers = self._get_request_headers(headers) | ||
return self._make_request( | ||
resource=resource, method="patch", headers=headers, params=params, data=data | ||
) | ||
|
||
def upsert(self, resource, data=None, headers=None): | ||
"""This method is dangerous! It is possible to delete and modify records | ||
en masse. Read the PostgREST docs.""" | ||
headers = self._get_request_headers(headers) | ||
if headers.get("Prefer"): | ||
headers["Prefer"] += ", resolution=merge-duplicates" | ||
else: | ||
headers["Prefer"] = "resolution=merge-duplicates" | ||
return self._make_request( | ||
resource=resource, method="post", headers=headers, data=data | ||
) | ||
|
||
def delete(self, resource, params=None, headers=None): | ||
"""This method is dangerous! It is possible to delete and modify records | ||
en masse. Read the PostgREST docs.""" | ||
if not params: | ||
raise Exception("You must supply parameters with delete requests. This is for your own protection.") | ||
|
||
url = f"{self.url}" | ||
self.res = requests.delete(self.url, params=params, headers=self.headers) | ||
self.res.raise_for_status() | ||
return self.res.json() | ||
|
||
|
||
def select(self, params=None, increment=1000, pagination=True): | ||
"""Select records from PostgREST DB. See documentation for horizontal | ||
raise Exception( | ||
"You must supply parameters with delete requests. This is for your own protection." # noqa E501 | ||
) | ||
headers = self._get_request_headers(headers) | ||
return self._make_request( | ||
resource=resource, method="delete", headers=headers, params=params | ||
) | ||
|
||
def select( | ||
self, *, resource, params=None, pagination=True, headers=None, | ||
): | ||
"""Fetch selected records from PostgREST. See documentation for horizontal | ||
and vertical filtering at http://postgrest.org/. | ||
Args: | ||
params (string): PostgREST-compliant request parametrs. | ||
increment (int, optional): The maximum number of records to | ||
return request per request. This is applied as a "limit" to | ||
each API request, until the user-specified limit is reached. | ||
Note that the PosgrREST DB itself will likely have limiting | ||
configured that cannot be exceeded. For example, our | ||
instances have a limit of 10000 records per request. | ||
pagination: | ||
If the client make multipel requets, returning multiple pages of | ||
results, buy using the `offest` param | ||
resource (str): Required. The postgrest's endpoint's table or view name to | ||
query. | ||
headers (dict): Custom PostgREST headers which will be passed to the | ||
request. Defaults to None. | ||
order_by (str): Field name to use a sort field when querying records. This | ||
must be provided when pagniation=True to ensure that the DB returns | ||
consistent results across all pages/offsets. | ||
pagination (bool): If the client should make repeated requests until etiher: | ||
- the limit param (if present) is met | ||
- if no limit param is included, until no more records are returned from | ||
the API. | ||
Defaults to True. | ||
params (dict): PostgREST-compliant request parameters. Defaults to None. | ||
Returns: | ||
TYPE: List | ||
List: A list of dicts of data returned from the host | ||
""" | ||
limit = params.setdefault('limit', 1000) | ||
|
||
params['limit'] = increment | ||
params = {} if not params else params | ||
limit = params.get("limit", math.inf) | ||
params.setdefault("offset", 0) | ||
|
||
params.setdefault('offset', 0) | ||
if pagination and not params.get("order"): | ||
raise ValueError( | ||
"It's unsafe to paginate requests without specifying an 'order' param" | ||
) | ||
|
||
records = [] | ||
headers = self._get_request_headers(headers) | ||
|
||
while True: | ||
self.res = requests.get(self.url, params=params, headers=self.headers) | ||
|
||
self.res.raise_for_status() | ||
|
||
records += self.res.json() | ||
|
||
if not self.res.json() or len(records) >= limit or not pagination: | ||
data = self._make_request( | ||
resource=resource, method="get", headers=headers, params=params | ||
) | ||
records += data | ||
|
||
if not data or len(records) >= limit or not pagination: | ||
# Postgrest has a max-rows configuration setting which limits the total | ||
# number of rows that can be returned from a request. when the the | ||
# client specifies a limit higher than max-rows, the the max-rows # of | ||
# rows are returned | ||
return records | ||
else: | ||
params['offset'] += len(self.res.json()) | ||
params["offset"] += len(data) |
Oops, something went wrong.