Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
*.pyc
|
||||
|
||||
*.sublime-project
|
||||
|
||||
*.sublime-workspace
|
||||
29
LICENSE.md
Normal file
29
LICENSE.md
Normal file
@@ -0,0 +1,29 @@
|
||||
The Attract Python SDK is released under the BSD License:
|
||||
|
||||
Copyright (c) 2015, Blender Foundation
|
||||
Copyright (c) 2013-2014, PayPal
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those
|
||||
of the authors and should not be interpreted as representing official policies,
|
||||
either expressed or implied, of the FreeBSD Project.
|
||||
2
README.md
Normal file
2
README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Attract Python REST SDK
|
||||
Integrate this module in your Python app to communicate with an Attract server.
|
||||
3
attractsdk/__init__.py
Normal file
3
attractsdk/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from attractsdk.api import Api
|
||||
from attractsdk.exceptions import ResourceNotFound, UnauthorizedAccess, MissingConfig
|
||||
from attractsdk.config import __version__, __pypi_packagename__
|
||||
188
attractsdk/api.py
Normal file
188
attractsdk/api.py
Normal file
@@ -0,0 +1,188 @@
|
||||
import base64
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import attractsdk.utils as utils
|
||||
from attractsdk import exceptions
|
||||
from attractsdk.config import __version__
|
||||
|
||||
|
||||
class Api(object):
|
||||
|
||||
# User-Agent for HTTP request
|
||||
library_details = "requests {0}; python {1}".format(
|
||||
requests.__version__, platform.python_version())
|
||||
user_agent = "Attract-Python-SDK/{0} ({1})".format(
|
||||
__version__, library_details)
|
||||
_api_singleton = None
|
||||
def __init__(self, options=None, **kwargs):
|
||||
"""Create API object
|
||||
|
||||
Usage::
|
||||
|
||||
>>> from attractsdk import Api
|
||||
>>> Api.Default(
|
||||
endpoint="http://localhost:5000",
|
||||
username='USERNAME',
|
||||
password='PASSWORD'
|
||||
)
|
||||
"""
|
||||
kwargs = utils.merge_dict(options or {}, kwargs)
|
||||
|
||||
self.endpoint = kwargs["endpoint"]
|
||||
self.username = kwargs["username"]
|
||||
self.password = kwargs["password"]
|
||||
self.token = kwargs["token"] if kwargs.get("token") else None
|
||||
self.options = kwargs
|
||||
|
||||
@staticmethod
|
||||
def Default(**kwargs):
|
||||
"""Initialize the API in a singleton style
|
||||
"""
|
||||
if Api._api_singleton is None:
|
||||
try:
|
||||
Api._api_singleton = Api(
|
||||
endpoint=kwargs["endpoint"],
|
||||
username=kwargs["username"],
|
||||
password=kwargs["password"])
|
||||
except KeyError:
|
||||
#raise exceptions.MissingConfig("Missing configuration value")
|
||||
print("Missing configuration value. Initialize with Api.Default().")
|
||||
sys.exit(0)
|
||||
|
||||
return Api._api_singleton
|
||||
|
||||
|
||||
def basic_auth(self):
|
||||
"""Returns base64 encoded token. Used to encode auth credentials
|
||||
for retrieving the token.
|
||||
"""
|
||||
credentials = "{0}:{1}".format(self.username, self.password)
|
||||
return base64.b64encode(credentials.encode('utf-8')).decode('utf-8').replace("\n", "")
|
||||
|
||||
def get_token(self):
|
||||
"""Generate new token by making a POST request
|
||||
"""
|
||||
path = "/users"
|
||||
payload = None
|
||||
|
||||
if self.token:
|
||||
return self.token
|
||||
else:
|
||||
# If token is not set we do initial request with username and password
|
||||
self.token = self.http_call(
|
||||
utils.join_url(self.endpoint, path), "POST",
|
||||
data=payload,
|
||||
headers={
|
||||
"Authorization": ("Basic {0}".format(self.basic_auth())),
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": self.user_agent
|
||||
})
|
||||
|
||||
return self.token
|
||||
|
||||
def request(self, url, method, body=None, headers=None):
|
||||
"""Make HTTP call, formats response and does error handling.
|
||||
Uses http_call method in API class.
|
||||
"""
|
||||
|
||||
http_headers = utils.merge_dict(self.headers(), headers or {})
|
||||
|
||||
if http_headers.get('Attract-Request-Id'):
|
||||
logging.info("Attract-Request-Id: {0}".format(http_headers['Attract-Request-Id']))
|
||||
|
||||
try:
|
||||
return self.http_call(url, method, data=json.dumps(body), headers=http_headers)
|
||||
|
||||
except exceptions.BadRequest as error:
|
||||
return {"error": json.loads(error.content)}
|
||||
|
||||
# Handle unauthorized token
|
||||
except exceptions.UnauthorizedAccess as error:
|
||||
raise error
|
||||
|
||||
def http_call(self, url, method, **kwargs):
|
||||
"""Makes a http call. Logs response information.
|
||||
"""
|
||||
# logging.info("Request[{0}]: {1}".format(method, url))
|
||||
response = requests.request(method, url, **kwargs)
|
||||
# logging.info("Response[{0}]: {1}".format(response.status_code, response.reason))
|
||||
return self.handle_response(response, response.content.decode('utf-8'))
|
||||
|
||||
def handle_response(self, response, content):
|
||||
"""Check HTTP response codes
|
||||
"""
|
||||
status = response.status_code
|
||||
if status in (301, 302, 303, 307):
|
||||
raise exceptions.Redirection(response, content)
|
||||
elif 200 <= status <= 299:
|
||||
return json.loads(content) if content else {}
|
||||
elif status == 400:
|
||||
raise exceptions.BadRequest(response, content)
|
||||
elif status == 401:
|
||||
raise exceptions.UnauthorizedAccess(response, content)
|
||||
elif status == 403:
|
||||
raise exceptions.ForbiddenAccess(response, content)
|
||||
elif status == 404:
|
||||
raise exceptions.ResourceNotFound(response, content)
|
||||
elif status == 405:
|
||||
raise exceptions.MethodNotAllowed(response, content)
|
||||
elif status == 409:
|
||||
raise exceptions.ResourceConflict(response, content)
|
||||
elif status == 410:
|
||||
raise exceptions.ResourceGone(response, content)
|
||||
elif status == 422:
|
||||
raise exceptions.ResourceInvalid(response, content)
|
||||
elif 401 <= status <= 499:
|
||||
raise exceptions.ClientError(response, content)
|
||||
elif 500 <= status <= 599:
|
||||
raise exceptions.ServerError(response, content)
|
||||
else:
|
||||
raise exceptions.ConnectionError(response,
|
||||
content, "Unknown response code: #{response.code}")
|
||||
|
||||
def headers(self):
|
||||
"""Default HTTP headers
|
||||
"""
|
||||
token = self.get_token()
|
||||
|
||||
return {
|
||||
"Authorization": ("Basic %s" % self.basic_auth(token=token)),
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"User-Agent": self.user_agent
|
||||
}
|
||||
|
||||
def get(self, action, headers=None):
|
||||
"""Make GET request
|
||||
"""
|
||||
return self.request(utils.join_url(self.endpoint, action), 'GET',
|
||||
headers=headers or {})
|
||||
|
||||
def post(self, action, params=None, headers=None):
|
||||
"""Make POST request
|
||||
"""
|
||||
return self.request(utils.join_url(self.endpoint, action), 'POST',
|
||||
body=params or {}, headers=headers or {}, refresh_token=refresh_token)
|
||||
|
||||
def put(self, action, params=None, headers=None):
|
||||
"""Make PUT request
|
||||
"""
|
||||
return self.request(utils.join_url(self.endpoint, action), 'PUT',
|
||||
body=params or {}, headers=headers or {}, refresh_token=refresh_token)
|
||||
|
||||
def patch(self, action, params=None, headers=None):
|
||||
"""Make PATCH request
|
||||
"""
|
||||
return self.request(utils.join_url(self.endpoint, action), 'PATCH',
|
||||
body=params or {}, headers=headers or {}, refresh_token=refresh_token)
|
||||
|
||||
def delete(self, action, headers=None):
|
||||
"""Make DELETE request
|
||||
"""
|
||||
return self.request(utils.join_url(self.endpoint, action), 'DELETE',
|
||||
headers=headers or {})
|
||||
5
attractsdk/config.py
Normal file
5
attractsdk/config.py
Normal file
@@ -0,0 +1,5 @@
|
||||
__version__ = "0.0.1"
|
||||
__pypi_username__ = "fsiddi"
|
||||
__pypi_packagename__ = "attract-sdk"
|
||||
__github_username__ = "fsiddi"
|
||||
__github_reponame__ = "Attract-Python-SDK"
|
||||
96
attractsdk/exceptions.py
Normal file
96
attractsdk/exceptions.py
Normal file
@@ -0,0 +1,96 @@
|
||||
|
||||
class ConnectionError(Exception):
|
||||
def __init__(self, response, content=None, message=None):
|
||||
self.response = response
|
||||
self.content = content
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
message = "Failed."
|
||||
if hasattr(self.response, 'status_code'):
|
||||
message += " Response status: %s." % (self.response.status_code)
|
||||
if hasattr(self.response, 'reason'):
|
||||
message += " Response message: %s." % (self.response.reason)
|
||||
if self.content is not None:
|
||||
message += " Error message: " + str(self.content)
|
||||
return message
|
||||
|
||||
|
||||
class Redirection(ConnectionError):
|
||||
"""3xx Redirection
|
||||
"""
|
||||
def __str__(self):
|
||||
message = super(Redirection, self).__str__()
|
||||
if self.response.get('Location'):
|
||||
message = "%s => %s" % (message, self.response.get('Location'))
|
||||
return message
|
||||
|
||||
|
||||
class MissingParam(TypeError):
|
||||
pass
|
||||
|
||||
|
||||
class MissingConfig(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ClientError(ConnectionError):
|
||||
"""4xx Client Error
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(ClientError):
|
||||
"""400 Bad Request
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UnauthorizedAccess(ClientError):
|
||||
"""401 Unauthorized
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ForbiddenAccess(ClientError):
|
||||
"""403 Forbidden
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceNotFound(ClientError):
|
||||
"""404 Not Found
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceConflict(ClientError):
|
||||
"""409 Conflict
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceGone(ClientError):
|
||||
"""410 Gone
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResourceInvalid(ClientError):
|
||||
"""422 Invalid
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ServerError(ConnectionError):
|
||||
"""5xx Server Error
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MethodNotAllowed(ClientError):
|
||||
"""405 Method Not Allowed
|
||||
"""
|
||||
|
||||
def allowed_methods(self):
|
||||
return self.response['Allow']
|
||||
235
attractsdk/resource.py
Normal file
235
attractsdk/resource.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import uuid
|
||||
|
||||
import attractsdk.utils as utils
|
||||
from attractsdk.api import Api
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for all REST services
|
||||
"""
|
||||
convert_resources = {}
|
||||
|
||||
def __init__(self, attributes=None):
|
||||
attributes = attributes or {}
|
||||
self.__dict__['api'] = Api.Default()
|
||||
|
||||
super(Resource, self).__setattr__('__data__', {})
|
||||
super(Resource, self).__setattr__('error', None)
|
||||
super(Resource, self).__setattr__('headers', {})
|
||||
super(Resource, self).__setattr__('header', {})
|
||||
super(Resource, self).__setattr__('request_id', None)
|
||||
self.merge(attributes)
|
||||
|
||||
def generate_request_id(self):
|
||||
"""Generate unique request id
|
||||
"""
|
||||
if self.request_id is None:
|
||||
self.request_id = str(uuid.uuid4())
|
||||
return self.request_id
|
||||
|
||||
def http_headers(self):
|
||||
"""Generate HTTP header
|
||||
"""
|
||||
return utils.merge_dict(self.header, self.headers,
|
||||
{'Attract-Request-Id': self.generate_request_id()})
|
||||
|
||||
def __str__(self):
|
||||
return self.__data__.__str__()
|
||||
|
||||
def __repr__(self):
|
||||
return self.__data__.__str__()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.__data__.get(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
try:
|
||||
# Handle attributes(error, header, request_id)
|
||||
super(Resource, self).__getattribute__(name)
|
||||
super(Resource, self).__setattr__(name, value)
|
||||
except AttributeError:
|
||||
self.__data__[name] = self.convert(name, value)
|
||||
|
||||
def success(self):
|
||||
return self.error is None
|
||||
|
||||
def merge(self, new_attributes):
|
||||
"""Merge new attributes e.g. response from a post to Resource
|
||||
"""
|
||||
for key, val in new_attributes.items():
|
||||
setattr(self, key, val)
|
||||
|
||||
def convert(self, name, value):
|
||||
"""Convert the attribute values to configured class
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
cls = self.convert_resources.get(name, Resource)
|
||||
return cls(value, api=self.api)
|
||||
elif isinstance(value, list):
|
||||
new_list = []
|
||||
for obj in value:
|
||||
new_list.append(self.convert(name, obj))
|
||||
return new_list
|
||||
else:
|
||||
return value
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__data__[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.__data__[key] = self.convert(key, value)
|
||||
|
||||
def to_dict(self):
|
||||
|
||||
def parse_object(value):
|
||||
if isinstance(value, Resource):
|
||||
return value.to_dict()
|
||||
elif isinstance(value, list):
|
||||
new_list = []
|
||||
for obj in value:
|
||||
new_list.append(parse_object(obj))
|
||||
return new_list
|
||||
else:
|
||||
return value
|
||||
|
||||
data = {}
|
||||
for key in self.__data__:
|
||||
data[key] = parse_object(self.__data__[key])
|
||||
return data
|
||||
|
||||
|
||||
class Find(Resource):
|
||||
|
||||
@classmethod
|
||||
def find(cls, resource_id):
|
||||
"""Locate resource, usually using ObjectID
|
||||
|
||||
Usage::
|
||||
|
||||
>>> Node.find("507f1f77bcf86cd799439011")
|
||||
"""
|
||||
|
||||
api = Api.Default()
|
||||
|
||||
url = utils.join_url(cls.path, str(resource_id))
|
||||
return cls(api.get(url))
|
||||
|
||||
|
||||
class List(Resource):
|
||||
|
||||
list_class = Resource
|
||||
|
||||
@classmethod
|
||||
def all(cls, params=None):
|
||||
"""Get list of resources, allowing some parameters such as:
|
||||
- count
|
||||
- start_time
|
||||
- sort_by
|
||||
- sort_order
|
||||
|
||||
Usage::
|
||||
|
||||
>>> shots = Nodes.all({'count': 2, 'type': 'shot'})
|
||||
"""
|
||||
api = Api.Default()
|
||||
|
||||
if params is None:
|
||||
url = cls.path
|
||||
else:
|
||||
url = utils.join_url_params(cls.path, params)
|
||||
|
||||
try:
|
||||
response = api.get(url)
|
||||
return cls.list_class(response)
|
||||
except AttributeError:
|
||||
# To handle the case when response is JSON Array
|
||||
if isinstance(response, list):
|
||||
new_resp = [cls.list_class(elem) for elem in response]
|
||||
return new_resp
|
||||
|
||||
|
||||
class Create(Resource):
|
||||
|
||||
def create(self):
|
||||
"""Create a resource
|
||||
|
||||
Usage::
|
||||
|
||||
>>> node = Node({})
|
||||
>>> node.create()
|
||||
"""
|
||||
|
||||
headers = self.http_headers()
|
||||
|
||||
new_attributes = self.api.post(self.path, self.to_dict(), headers)
|
||||
self.error = None
|
||||
self.merge(new_attributes)
|
||||
return self.success()
|
||||
|
||||
|
||||
class Update(Resource):
|
||||
"""Update a resource
|
||||
"""
|
||||
|
||||
def update(self, attributes=None):
|
||||
attributes = attributes or self.to_dict()
|
||||
url = utils.join_url(self.path, str(self['id']))
|
||||
new_attributes = self.api.put(url, attributes, self.http_headers())
|
||||
self.error = None
|
||||
self.merge(new_attributes)
|
||||
return self.success()
|
||||
|
||||
|
||||
class Replace(Resource):
|
||||
"""Partial update or modify resource
|
||||
see http://williamdurand.fr/2014/02/14/please-do-not-patch-like-an-idiot/
|
||||
|
||||
Usage::
|
||||
|
||||
>>> node = Node.find("507f1f77bcf86cd799439011")
|
||||
>>> node.replace([{'op': 'replace', 'path': '/name', 'value': 'Renamed Shot 2' }])
|
||||
"""
|
||||
|
||||
def replace(self, attributes=None):
|
||||
attributes = attributes or self.to_dict()
|
||||
url = utils.join_url(self.path, str(self['id']))
|
||||
new_attributes = self.api.patch(url, attributes, self.http_headers())
|
||||
self.error = None
|
||||
self.merge(new_attributes)
|
||||
return self.success()
|
||||
|
||||
|
||||
class Delete(Resource):
|
||||
|
||||
def delete(self):
|
||||
"""Delete a resource
|
||||
|
||||
Usage::
|
||||
|
||||
>>> node = Node.find("507f1f77bcf86cd799439011")
|
||||
>>> node.delete()
|
||||
"""
|
||||
url = utils.join_url(self.path, str(self['id']))
|
||||
new_attributes = self.api.delete(url)
|
||||
self.error = None
|
||||
self.merge(new_attributes)
|
||||
return self.success()
|
||||
|
||||
|
||||
class Post(Resource):
|
||||
|
||||
def post(self, name, attributes=None, cls=Resource, fieldname='id'):
|
||||
"""Constructs url with passed in headers and makes post request via
|
||||
post method in api class.
|
||||
"""
|
||||
attributes = attributes or {}
|
||||
url = utils.join_url(self.path, str(self[fieldname]), name)
|
||||
if not isinstance(attributes, Resource):
|
||||
attributes = Resource(attributes, api=self.api)
|
||||
new_attributes = self.api.post(url, attributes.to_dict(), attributes.http_headers())
|
||||
if isinstance(cls, Resource):
|
||||
cls.error = None
|
||||
cls.merge(new_attributes)
|
||||
return self.success()
|
||||
else:
|
||||
return cls(new_attributes, api=self.api)
|
||||
46
attractsdk/utils.py
Normal file
46
attractsdk/utils.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import re
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except ImportError:
|
||||
from urllib import urlencode
|
||||
|
||||
|
||||
def join_url(url, *paths):
|
||||
"""
|
||||
Joins individual URL strings together, and returns a single string.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> utils.join_url("attract:5000", "shots")
|
||||
attract:5000/shots
|
||||
"""
|
||||
for path in paths:
|
||||
url = re.sub(r'/?$', re.sub(r'^/?', '/', path), url)
|
||||
return url
|
||||
|
||||
|
||||
def join_url_params(url, params):
|
||||
"""Constructs a query string from a dictionary and appends it to a url.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> utils.join_url_params("attract:5000/shots", {"page-id": 2, "NodeType": "Shot Group"})
|
||||
attract:5000/shots?page-id=2&NodeType=Shot+Group
|
||||
"""
|
||||
return url + "?" + urlencode(params)
|
||||
|
||||
|
||||
def merge_dict(data, *override):
|
||||
"""
|
||||
Merges any number of dictionaries together, and returns a single dictionary.
|
||||
|
||||
Usage::
|
||||
|
||||
>>> utils.merge_dict({"foo": "bar"}, {1: 2}, {"foo1": "bar2"})
|
||||
{1: 2, 'foo': 'bar', 'foo1': 'bar2'}
|
||||
"""
|
||||
result = {}
|
||||
for current_dict in (data,) + override:
|
||||
result.update(current_dict)
|
||||
return result
|
||||
40
setup.py
Normal file
40
setup.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from distutils.core import setup
|
||||
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'attractsdk'))
|
||||
from config import __version__, __pypi_packagename__, __github_username__, __github_reponame__
|
||||
|
||||
long_description="""
|
||||
The Attract REST SDK provides Python APIs to communicate to the Attract webservices.
|
||||
"""
|
||||
|
||||
# license='Free BSD'
|
||||
# if os.path.exists('LICENSE.md'):
|
||||
# license = open('LICENSE.md').read()
|
||||
|
||||
url='https://github.com/' + __github_username__ + '/' + __github_reponame__
|
||||
|
||||
setup(
|
||||
name=__pypi_packagename__,
|
||||
version= __version__,
|
||||
author='Francesco Siddi, PayPal',
|
||||
author_email='francesco@blender.org',
|
||||
packages=['attractsdk'],
|
||||
scripts=[],
|
||||
url=url,
|
||||
license='BSD License',
|
||||
description='The Attract REST SDK provides Python APIs to communicate to the Attract webservices.',
|
||||
long_description=long_description,
|
||||
install_requires=['requests>=1.0.0', 'six>=1.0.0', 'pyopenssl>=0.14'],
|
||||
classifiers=[
|
||||
'Intended Audience :: Developers',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules'
|
||||
],
|
||||
keywords=['attract', 'rest', 'sdk', 'tracking', 'film', 'production']
|
||||
)
|
||||
Reference in New Issue
Block a user