diff --git a/.travis.yml b/.travis.yml index 72ac06d..1de12e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,9 +12,6 @@ git: jobs: include: - stage: test - python: "2.7" - script: sh test.sh - if: env(deploy) IS NOT present - # stage name not required python: "3.4" script: sh test.sh diff --git a/README.rst b/README.rst index dd29262..3c1252c 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ applications. System Requirements ------------------- -The SDK currently supports Python 2.7, 3.4, 3.5, 3.6, pypy, and pypy3. +The SDK currently supports Python 3.4, 3.5, 3.6, pypy, and pypy3. The following packages are required. - `requests `__ diff --git a/setup.py b/setup.py index 50d2053..788c938 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,6 @@ def run_tests(self): 'Operating System :: OS Independent', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', diff --git a/smartsheet-sdk-tests b/smartsheet-sdk-tests new file mode 160000 index 0000000..8044e89 --- /dev/null +++ b/smartsheet-sdk-tests @@ -0,0 +1 @@ +Subproject commit 8044e8962393e0bcb5e1f250ca7d84f837cbd213 diff --git a/smartsheet/models/date_object_value.py b/smartsheet/models/date_object_value.py index ed20abd..ff450c3 100644 --- a/smartsheet/models/date_object_value.py +++ b/smartsheet/models/date_object_value.py @@ -26,9 +26,9 @@ class DateObjectValue(ObjectValue): """Smartsheet DateObjectValue data model.""" - def __init__(self, props=None, object_type=None, base_obj=None): + def __init__(self, props=None, base_obj=None): """Initialize the DateObjectValue model.""" - super(DateObjectValue, self).__init__(object_type, base_obj) + super(DateObjectValue, self).__init__(base_obj) self._base = None if base_obj is not None: self._base = base_obj @@ -50,5 +50,5 @@ def value(self, value): self._value = value else: if isinstance(value, six.string_types): - value = parse(value) + value = parse(value).date() self._value = value diff --git a/smartsheet/models/datetime_object_value.py b/smartsheet/models/datetime_object_value.py new file mode 100644 index 0000000..828c956 --- /dev/null +++ b/smartsheet/models/datetime_object_value.py @@ -0,0 +1,55 @@ + +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2018Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from .object_value import * +from ..util import deserialize +from datetime import datetime +from dateutil.parser import parse + + +class DatetimeObjectValue(ObjectValue): + """Smartsheet DateObjectValue data model.""" + + def __init__(self, props=None, object_type=None, base_obj=None): + """Initialize the DateObjectValue model.""" + super(DatetimeObjectValue, self).__init__(object_type, base_obj) + self._base = None + if base_obj is not None: + self._base = base_obj + + self._value = None + + if props: + deserialize(self, props) + + self.__initialized = True + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + if isinstance(value, datetime): + self._value = value + else: + if isinstance(value, six.string_types): + value = parse(value) + self._value = value \ No newline at end of file diff --git a/smartsheet/models/sheet.py b/smartsheet/models/sheet.py index b3f457f..70c846b 100644 --- a/smartsheet/models/sheet.py +++ b/smartsheet/models/sheet.py @@ -20,6 +20,7 @@ from .attachment import Attachment from .column import Column from .sheet_filter import SheetFilter +from .sheet_form import SheetForm from .comment import Comment from .contact_object_value import ContactObjectValue from .cross_sheet_reference import CrossSheetReference @@ -61,6 +62,7 @@ def __init__(self, props=None, base_obj=None): self._effective_attachment_options = EnumeratedList(AttachmentType) self._favorite = Boolean() self._filters = TypedList(SheetFilter) + self._forms = TypedList(SheetForm) self._from_id = Number() self._gantt_enabled = Boolean() self._has_summary_fields = Boolean() @@ -190,6 +192,14 @@ def filters(self): def filters(self, value): self._filters.load(value) + @property + def forms(self): + return self._forms + + @forms.setter + def forms(self, value): + self._forms.load(value) + @property def from_id(self): return self._from_id.value diff --git a/smartsheet/models/sheet_form.py b/smartsheet/models/sheet_form.py new file mode 100644 index 0000000..44ecbe6 --- /dev/null +++ b/smartsheet/models/sheet_form.py @@ -0,0 +1,115 @@ +# pylint: disable=C0111,R0902,R0904,R0912,R0913,R0915,E1101 +# Smartsheet Python SDK. +# +# Copyright 2017 Smartsheet.com, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"): you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from __future__ import absolute_import + +from ..types import * +from ..util import serialize +from ..util import deserialize + + +class SheetForm(object): + + """Smartsheet SheetForm data model.""" + + def __init__(self, props=None, base_obj=None): + """Initialize the SheetForm model.""" + self._base = None + if base_obj is not None: + self._base = base_obj + + self._id = Number() + self._publish_type = String() + self._publish_key = String() + self._publish_url = String() + self._title = String() + self._published = Boolean() + + if props: + deserialize(self, props) + + self.request_response = None + self.__initialized = True + + # def __getattr__(self, key): + # if key == 'id': + # return self.id_ + # else: + # raise AttributeError(key) + + # def __setattr__(self, key, value): + # if key == 'id': + # self.id_ = value + # else: + # super(SheetForm, self).__setattr__(key, value) + + @property + def id(self): + return self._id + + @id.setter + def id(self, value): + self._id.value = value + + @property + def publish_type(self): + return self._publish_type.value + + @publish_type.setter + def publish_type(self, value): + self._publish_type.value = value + + @property + def publish_key(self): + return self._publish_key.value + + @publish_key.setter + def publish_key(self, value): + self._publish_key.value = value + + @property + def publish_url(self): + return self._publish_url + + @publish_url.setter + def publish_url(self, value): + self._publish_url.value = value + + @property + def title(self): + return self._title + + @title.setter + def title(self, value): + self._title.value = value + + @property + def published(self): + return self._published + + @published.setter + def published(self, value): + self._published.value = value + + def to_dict(self): + return serialize(self) + + def to_json(self): + return json.dumps(self.to_dict()) + + def __str__(self): + return self.to_json() \ No newline at end of file diff --git a/smartsheet/object_value.py b/smartsheet/object_value.py index ada13e7..a7b2aea 100644 --- a/smartsheet/object_value.py +++ b/smartsheet/object_value.py @@ -17,6 +17,7 @@ from .models.boolean_object_value import BooleanObjectValue from .models.contact_object_value import ContactObjectValue from .models.date_object_value import DateObjectValue +from .models.datetime_object_value import DatetimeObjectValue from .models.duration import Duration from .models.multi_contact_object_value import MultiContactObjectValue from .models.multi_picklist_object_value import MultiPicklistObjectValue @@ -40,9 +41,11 @@ def assign_to_object_value(value): return PredecessorList(value) elif enum_object_type == CONTACT: return ContactObjectValue(value) - elif enum_object_type == DATE or enum_object_type == DATETIME or \ + elif enum_object_type == DATE: + return DateObjectValue(value) + elif enum_object_type == DATETIME or \ enum_object_type == ABSTRACT_DATETIME: - return DateObjectValue(value, enum_object_value_type) + return DatetimeObjectValue(value, enum_object_value_type) elif enum_object_type == MULTI_CONTACT: return MultiContactObjectValue(value) elif enum_object_type == MULTI_PICKLIST: diff --git a/smartsheet/session.py b/smartsheet/session.py index 7305589..443075e 100644 --- a/smartsheet/session.py +++ b/smartsheet/session.py @@ -50,7 +50,7 @@ def pinned_session(pool_maxsize=8): http_adapter = _SSLAdapter(pool_connections=4, pool_maxsize=pool_maxsize, max_retries=Retry(total=1, - method_whitelist=Retry.DEFAULT_METHOD_WHITELIST.union(['POST']))) + allowed_methods=Retry.DEFAULT_ALLOWED_METHODS.union(['POST']))) _session = requests.session() _session.hooks = {'response': redact_token} diff --git a/smartsheet/sheets.py b/smartsheet/sheets.py index 0d0e67e..7c610ef 100644 --- a/smartsheet/sheets.py +++ b/smartsheet/sheets.py @@ -430,7 +430,7 @@ def get_row(self, sheet_id, row_id, include=None, exclude=None): flags that indicate additional attributes to be included in each Row object within the response. Valid list values: discussions, attachments, format, filters, columnType, - rowPermalink, rowWriterInfo. + rowPermalink, rowPropertyReleaseFlag, rowWriterInfo. exclude (str): Response will not include cells that have never contained any data. diff --git a/smartsheet/smartsheet.py b/smartsheet/smartsheet.py index e67be7c..1de3a09 100644 --- a/smartsheet/smartsheet.py +++ b/smartsheet/smartsheet.py @@ -173,6 +173,7 @@ def __init__(self, access_token=None, max_connections=8, self._assume_user = None self._test_scenario_name = None self._change_agent = None + self._custom_properties = None def assume_user(self, email=None): """Assume identity of specified user. @@ -218,11 +219,20 @@ def with_change_agent(self, change_agent): """ Request headers will contain the 'Smartsheet-Change-Agent' header value - Agrs: + Args: change_agent: (str) the name of this change agent """ self._change_agent = change_agent + def with_custom_properties(self, custom_properties): + """ + Request headers will contain custom header values defined in the 'custom_properties' + + Args: + 'custom_properties': (dict) the dictionary of custom headers + """ + self._custom_properties = custom_properties + def request(self, prepped_request, expected, operation): """ Make a request from the Smartsheet API. @@ -416,6 +426,11 @@ def prepare_request(self, _op): except KeyError: pass + if self._custom_properties is not None: + for custom_property in self._custom_properties: + prepped_request.headers.update( + {custom_property: self._custom_properties[custom_property]}) + return prepped_request def __getattr__(self, name): diff --git a/smartsheet/token.py b/smartsheet/token.py index c67588b..397c26e 100644 --- a/smartsheet/token.py +++ b/smartsheet/token.py @@ -20,6 +20,7 @@ import logging from . import fresh_operation +import hashlib class Token(object): @@ -69,6 +70,45 @@ def get_access_token(self, client_id, code, _hash, redirect_uri=None): return response + def get_access_token(self, client_id, code, client_secret, redirect_uri=None): + """Get an access token, as part of the OAuth process. For more + information, see [OAuth + Flow](http://smartsheet-platform.github.io/api-docs/index.html#oauth-flow) + + Args: + client_id (str) + code (str) + _client_secret (str): Plain text of your `app_secret`. + redirect_uri (str): Redirect URL registered for + your app, including protocol (e.g. \"http://\"); if not + provided, the redirect URL set during registration is used. + + Returns: + AccessToken + """ + if not all(val is not None for val in ['client_id', 'code', 'client_secret']): + raise ValueError( + ('One or more required values ' + 'are missing from call to ' + __name__)) + app_secret = hashlib.sha256(client_secret + '|' + code) + + _op = fresh_operation('get_access_token') + _op['method'] = 'POST' + _op['path'] = '/token' + _op['form_data'] = {} + _op['form_data']['grant_type'] = 'authorization_code' + _op['form_data']['client_id'] = client_id + _op['form_data']['code'] = code + _op['form_data']['redirect_uri'] = redirect_uri + _op['form_data']['hash'] = app_secret + _op['auth_settings'] = None + + expected = 'AccessToken' + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response + def refresh_access_token(self, client_id, refresh_token, _hash, redirect_uri=None): """Refresh an access token, as part of the OAuth process. For more @@ -109,6 +149,47 @@ def refresh_access_token(self, client_id, refresh_token, _hash, return response + def refresh_access_token(self, client_id, refresh_token, client_secret, + redirect_uri=None): + """Refresh an access token, as part of the OAuth process. For more + information, see [OAuth + Flow](http://smartsheet-platform.github.io/api-docs/index.html#oauth-flow) + + Args: + client_id (str) + refresh_token (str) + client_secret (str): Plain text of your `app_secret` + redirect_uri (str): Redirect URL registered for + your app, including protocol (e.g. \"http://\"); if not + provided, the redirect URL set during registration is used. + + Returns: + AccessToken + """ + if not all(val is not None for val in ['client_id', 'refresh_token', + 'client_secret']): + raise ValueError( + ('One or more required values ' + 'are missing from call to ' + __name__)) + + app_secret = hashlib.sha256(client_secret + '|' + code) + + _op = fresh_operation('refresh_access_token') + _op['method'] = 'POST' + _op['path'] = '/token' + _op['form_data'] = {} + _op['form_data']['grant_type'] = 'refresh_token' + _op['form_data']['client_id'] = client_id + _op['form_data']['refresh_token'] = refresh_token + _op['form_data']['redirect_uri'] = redirect_uri + _op['form_data']['hash'] = app_secret + + expected = 'AccessToken' + prepped_request = self._base.prepare_request(_op) + response = self._base.request(prepped_request, expected, _op) + + return response + def revoke_access_token(self): """Revoke the access token used to make the request. diff --git a/smartsheet/types.py b/smartsheet/types.py index 598e42c..9cdd2fd 100644 --- a/smartsheet/types.py +++ b/smartsheet/types.py @@ -15,7 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections import importlib import json import logging @@ -25,8 +24,12 @@ from dateutil.parser import parse from enum import Enum +try: + from collections.abc import MutableSequence +except ImportError: + from collections import MutableSequence -class TypedList(collections.MutableSequence): +class TypedList(MutableSequence): def __init__(self, item_type): self.item_type = item_type @@ -281,7 +284,7 @@ def set(self, value): self._value = None def __eq__(self, other): - if isinstance(other, Enum): + if isinstance(other, Enum) or other is None: return self._value == other elif isinstance(other, six.string_types): return self._value == self.__enum[other]