Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions .github/workflows/citests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Payload Python Module

on:
pull_request:
branches: ["master"]
push:
branches: ["master"]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: "3.11"
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
python3 -m pip install --upgrade pdm
pip install flake8
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Install app dependencies
run: |
pdm install
- name: Test with pytest
env:
TEST_SECRET_KEY: ${{ secrets.API_KEY }}
run: |
pdm run pytest tests/
29 changes: 29 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Publish Package to npmjs
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: "3.11"
- run: |
sudo apt install python3-build twine

echo << EOF
[distutils]
index-servers =
payload-api
[payload-api]
repository = https://upload.pypi.org/legacy/
username = __token__
password = $PYPI_TOKEN
EOF > $HOME/.pypirc
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
- run: python3 -m build
- run: python3 -m twine upload --repository payload-api dist/*
105 changes: 58 additions & 47 deletions payload/arm/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,51 @@
import requests
import copy

from ..utils import (convert_fieldmap, map_attrs, map_object,
nested_qstring_keys, object2data)
from ..utils import convert_fieldmap, map_object, nested_qstring_keys, object2data
from .attr import Attr
from .object import ARMObject

if sys.version_info >= (3,0):
if sys.version_info >= (3, 0):
from urllib.parse import urljoin
else:
from urlparse import urljoin

class ARMRequest(object):

class ARMRequest(object):
def __init__(self, Object=None, session=None):
self.Object = Object
self.session = session
self._filters = []
self._attrs = []
self._filters = []
self._attrs = []
self._group_by = []

def _request(self, method, id=None, headers=None, params=None, json=None):
session = self.session or payload
session = self.session or payload
endpoint = self.Object.__spec__['endpoint']
headers = headers or {}
params = nested_qstring_keys(params or {})
auth = (session.api_key, '')
files = {}
headers = headers or {}
params = nested_qstring_keys(params or {})
auth = (session.api_key, '')
files = {}

if json:
flat_data = nested_qstring_keys(copy.copy(json))
for k in list(flat_data):
if hasattr(flat_data[k], 'read'): files[k] = flat_data.pop(k)
if hasattr(flat_data[k], 'read'):
files[k] = flat_data.pop(k)

if id: endpoint = f"{endpoint}/{id}"
if id:
endpoint = f'{endpoint}/{id}'

if self._filters:
for k, v in dict( (f.attr, f.opval) for f in self._filters ).items():
if k not in params: params[k] = v
for k, v in dict((f.attr, f.opval) for f in self._filters).items():
if k not in params:
params[k] = v

if self._attrs:
params['fields'] = list(map(str,self._attrs))
params['fields'] = list(map(str, self._attrs))

if self._group_by:
params['group_by'] = list(map(str,self._group_by))
params['group_by'] = list(map(str, self._group_by))

convert_fieldmap(params, self.Object.field_map)
if json:
Expand All @@ -61,16 +63,16 @@ def _request(self, method, id=None, headers=None, params=None, json=None):
params=params,
auth=auth,
data=flat_data,
files=files
)
files=files,
)
else:
response = getattr(requests, method)(
urljoin(session.api_url, endpoint.strip('/')),
headers=headers,
params=params,
auth=auth,
json=json
)
json=json,
)
try:
data = response.json()

Expand All @@ -91,10 +93,16 @@ def _request(self, method, id=None, headers=None, params=None, json=None):
data['_session'] = self.session
return map_object(data)
else:
errors = [ err for _ in payload.PayloadError.__subclasses__() for err in _.__subclasses__()+[_] ]
errors = [
err
for _ in payload.PayloadError.__subclasses__()
for err in _.__subclasses__() + [_]
]
for Error in errors:
if Error.__name__ != data.get('error_type') \
or Error.http_code != response.status_code:
if (
Error.__name__ != data.get('error_type')
or Error.http_code != response.status_code
):
continue
raise Error(data.get('description'), data)
if response.status_code == 500:
Expand All @@ -116,71 +124,74 @@ def group_by(self, *fields):

def create(self, obj=None, **values):
obj = obj or values
if isinstance( obj, list ):
if isinstance(obj, list):
for o in obj:
if not self.Object and not isinstance( o, ARMObject ):
if not self.Object and not isinstance(o, ARMObject):
raise TypeError('Bulk create requires ARMObject object types')
if not self.Object:
self.Object = o.__class__
elif isinstance( o, dict ):
o.update(self.Object.__spec__.get('polymorphic',{}))
elif not isinstance( o, self.Object ):
elif isinstance(o, dict):
o.update(self.Object.__spec__.get('polymorphic', {}))
elif not isinstance(o, self.Object):
raise TypeError('Bulk create requires all objects to be of the same type')
obj = { 'object': 'list', 'values': obj }
elif isinstance( obj, dict ):
obj.update(self.Object.__spec__.get('polymorphic',{}))
obj = {'object': 'list', 'values': obj}
elif isinstance(obj, dict):
obj.update(self.Object.__spec__.get('polymorphic', {}))

obj = object2data(obj)
return self._request('post', json=obj)

def delete(self, objects=None):
if isinstance( objects, list ):
if isinstance(objects, list):
if not objects:
raise ValueError('List must not be empty')
for o in objects:
if not isinstance( o, ARMObject ):
if not isinstance(o, ARMObject):
raise TypeError('Bulk delete requires ARMObject object types')
if not self.Object:
self.Object = o.__class__
elif not isinstance( o, self.Object ):
elif not isinstance(o, self.Object):
raise TypeError('Bulk delete requires all objects to be of the same type')
delete_query = '|'.join([ obj.id for obj in objects ])
delete_query = '|'.join([obj.id for obj in objects])
return self._request('delete', params={'id': delete_query, 'mode': 'query'})
elif isinstance( objects, ARMObject ):
elif isinstance(objects, ARMObject):
self.Object = objects.__class__
return self._request('delete', id=objects.id)
elif objects is None and self.Object and self._filters:
return self._request('delete', params={'mode':'query'})
return self._request('delete', params={'mode': 'query'})
else:
raise TypeError('Bulk delete requires ARMObject object types')

def update(self, objects=None, **values):
if objects:
if not isinstance( objects, list ):
if not isinstance(objects, list):
raise ValueError('first parameter must be a list of updates')
if not objects or not isinstance( objects[0], (list, tuple) ):
if not objects or not isinstance(objects[0], (list, tuple)):
raise ValueError('first parameter must be a list of updates')
for o, upd in objects:
if not isinstance( o, ARMObject ):
if not isinstance(o, ARMObject):
raise TypeError('Bulk update requires ARMObject object types')
if not self.Object:
self.Object = o.__class__
elif not isinstance( o, self.Object ):
elif not isinstance(o, self.Object):
raise TypeError('Bulk update requires all objects to be of the same type')
updates = { 'object': 'list', 'values':
list(map( lambda upd: dict( upd[1], id=upd[0].id ), objects )) }
updates = {
'object': 'list',
'values': list(map(lambda upd: dict(upd[1], id=upd[0].id), objects)),
}
return self._request('put', json=updates)
return self._request('put', params={'mode':'query'}, json=values)
return self._request('put', params={'mode': 'query'}, json=values)

def filter_by(self, *filters, **kw_filters):
self._filters.extend(filters)
for key, val in nested_qstring_keys(kw_filters).items():
self._filters.append( getattr( Attr, key ) == val )
self._filters.append(getattr(Attr, key) == val)
return self

def all(self):
return self._request('get')

def first(self):
resp = self._request('get', params=dict(limit=1))
if resp: return resp[0]
if resp:
return resp[0]
47 changes: 19 additions & 28 deletions payload/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .arm.object import ARMObject, _object_cache
from .arm.attr import Attr


def get_object_cls(item):
cls = None
for Object in _object_cache:
Expand All @@ -19,15 +20,17 @@ def get_object_cls(item):
cls = Object
return cls


def map_object(item):
cls = get_object_cls(item)
if cls:
return cls(**item)
return item


def nested_qstring_keys(base):
def recurse(item, fmt='{}'):
if isinstance( item, (list,tuple) ):
if isinstance(item, (list, tuple)):
_iter = enumerate(item)
else:
_iter = list(item.items())
Expand All @@ -38,45 +41,51 @@ def recurse(item, fmt='{}'):
val = str(Attr)
base[new_key] = val
else:
if item is base: del base[key]
recurse(val, new_key+'[{}]')
if item is base:
del base[key]
recurse(val, new_key + '[{}]')
return base

return recurse(base)


def data2object(item, field_map=set(), session=None):
def recurse(item):
if isinstance( item, (list,tuple) ):
if isinstance(item, (list, tuple)):
_iter = enumerate(item)
else:
convert_fieldmap(item, field_map)
_iter = list(item.items())
for key, val in _iter:
if isinstance(val, (list, dict)):
item[key] = recurse(val)
if isinstance(val, dict)\
and val.get('object'):
if isinstance(val, dict) and val.get('object'):
Object = get_object_cls(val)
if Object:
if session:
val['_session'] = session
item[key] = Object(**val)
return item

return recurse(item)


def convert_fieldmap(obj, field_map):
for f in field_map:
if 'type' not in obj: continue
if 'type' not in obj:
continue
if obj['type'] not in obj:
obj[obj['type']] = {}
if f in obj:
obj[obj['type']][f] = obj.pop(f)


def object2data(item):
def recurse(item):
if isinstance( item, (list,tuple) ):
if isinstance(item, (list, tuple)):
_iter = enumerate(item)
else:
if isinstance( item, ARMObject ):
if isinstance(item, ARMObject):
item = item.data()
_iter = list(item.items())
for key, val in _iter:
Expand All @@ -85,23 +94,5 @@ def recurse(item):
elif isinstance(val, (list, dict)):
item[key] = recurse(val)
return item
return recurse(item)

def map_attrs( cls, item ):
if isinstance( item, cls ):
item = item.data()
for key in item:
if isinstance(item[key], ARMObject):
map_attrs(item[key].__class__, item[key])
continue
if not cls.__spec__.get('attr_map')\
or key not in cls.attr_map:
continue
key_map = cls.__spec__['attr_map'][key]
if isinstance( new_key, str ):
obj[key_map] = item.pop(key)
else:
nested_key = list(key_map.keys())[0]
if nested_key not in obj:
obj[nested_key] = {}
obj[nested_key][key_map[nested_key]] = obj.pop(key)
return recurse(item)
Loading