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

Optionally Allow Direct Passing of Values with EnumEnforcer #170

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,7 @@ tags
virtualenv

docs-build/

# Common IDE/editor data directories
.vscode/
.idea/
4 changes: 2 additions & 2 deletions tda/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ class BaseClient(EnumEnforcer):
checking status codes. For methods which support responses, they can be
found in the response object's ``json()`` method.'''

def __init__(self, api_key, session, *, enforce_enums=True):
def __init__(self, api_key, session, *, enforce_enums=True, allow_values=False):
'''Create a new client with the given API key and session. Set
`enforce_enums=False` to disable strict input type checking.'''
super().__init__(enforce_enums)
super().__init__(enforce_enums, allow_values=allow_values)

self.api_key = api_key
self.session = session
Expand Down
138 changes: 112 additions & 26 deletions tda/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,133 @@


class EnumEnforcer:
def __init__(self, enforce_enums):
'''Ensures values passed to the enum conversion methods are members of the
correct enum type with ``enforce_enums=True``. With ``allow_values=True``,
allows directly passing member values instead, but rejects values which are not
defined in the relevant enumeration. With `enforce_enums=False`, any value is
allowed (and `allow_values` is ignored).

:param enforce_enums: True to enable parameter validation, False otherwise.
:param allow_values: True to allow direct passing of enumerated values, False to
only allow members of the appropriate enum. Ignored with ``enforce_enums=False``.
'''
def __init__(self, enforce_enums, allow_values=False):
self.enforce_enums = enforce_enums

def type_error(self, value, required_enum_type):
raise ValueError(
('expected type "{}", got type "{}" (initialize with ' +
'enforce_enums=True to disable this checking)').format(
required_enum_type.__name__,
type(value).__name__))

def convert_enum(self, value, required_enum_type):
self.allow_values = allow_values

def _raise_unrecognized_value(self, value, enum_type):
error_str = ('Value "{actual}" is not is not enumerated in {enum}. Check the '
'official TD Ameritrade API documentation to see what values are '
'allowed. If the value you need is missing from {enum}, you can '
'disable parameter validation by initializing {subclass} with '
'enforce_enums=False. However, if an allowed value is truly not '
'enumerated in {enum}, please submit an issue to {repo_url} '
'including the name of the enum, a link to the relevant '
'official documentation, and the missing value.')
raise UnrecognizedValueException(
error_str.format(
actual=value,
enum=enum_type.__name__,
subclass=self.__class__.__name__,
repo_url="https://github.com/alexgolec/tda-api",
)
)

def _raise_enum_required(self, value, required_enum_type):
error_str = ('Expected type {expected}, got type "{actual}". Initialize '
'{subclass} with allow_values=True to allow direct usage of '
'enumerated values, or use enforce_enums=False to disable '
'validation altogether. Check the official TD Ameritrade API '
'documentation to see what values are allowed. If an allowed '
'value is truly not enumerated in {expected}, please submit an '
'issue to {repo_url} including the name of the enum, a link to '
'the relevant official documentation, and the missing value.')
raise EnumRequiredException(
error_str.format(
expected=required_enum_type.__name__,
actual=type(value).__name__,
subclass=self.__class__.__name__,
repo_url="https://github.com/alexgolec/tda-api"
)
)

def convert_enum(self, value, required_enum_type, allowed_values=None):
'''Validate the given ``value`` using ``required_enum_type``.

With ``self.enforce_enums=True``, check that the given ``value`` is a member of
the required type, and return its value if so.

With ``self.allow_values=True``, also allows ``value`` to be an enumerated value
in the required enum type, returning ``value`` back unchanged if so.

With ``self.enforce_enums=False``, if ``value`` is a member of the expected enum,
returns ``value.value``, otherwise, returns ``value`` unchanged.

:param value: The object in question.
:param required_enum_type: The expected enum type.
:param allowed_values: The set of values which are allowed with
``self.allow_values=True``, or None to use the set of all values enumerated
in ``required_enum_type``. Providing this set is useful to avoid repeated
recomputation of allowed values. Ignored if value validation is turned off.
:raise EnumRequiredException: if ``value`` is not a member of the required enum
type, ``self.enforce_enums=True``, and ``self.allow_values=False``.
:raise UnrecognizedValueException: if ``value`` is not a member of the required
enum type nor one of its enumerated values, and ``self.enforce_enums=True``,
and ``self.allow_values=True``.
:return: either ``value`` or ``value.value`` as appropriate.
'''
if value is None:
return None

if isinstance(value, required_enum_type):
return value.value
elif self.enforce_enums:
self.type_error(value, required_enum_type)
else:
return value
if self.enforce_enums:
if self.allow_values:
if allowed_values is None:
allowed_values = set(member.value for member in required_enum_type)
if value in allowed_values:
return value
self._raise_unrecognized_value(value, required_enum_type)
self._raise_enum_required(value, required_enum_type)
return value

def convert_enum_iterable(self, iterable, required_enum_type):
'''Return the ``iterable`` of parameters with all elements converted as needed
by :meth:`EnumEnforcer.convert_enum_iterable`.

If ``iterable`` is a member of the appropriate enum (not an iterable), returns a
list containg ``iterable`` as its only element. This does not work when passing
values directly--even a single value must be contained in an iterable.
'''
if iterable is None:
return None

if isinstance(iterable, required_enum_type):
return [iterable.value]

values = []
for value in iterable:
if isinstance(value, required_enum_type):
values.append(value.value)
elif self.enforce_enums:
self.type_error(value, required_enum_type)
else:
values.append(value)
return values
allowed_values = set(member.value for member in required_enum_type)
return [
self.convert_enum(value, required_enum_type, allowed_values=allowed_values)
for value in iterable
]

def set_enforce_enums(self, enforce_enums):
self.enforce_enums = enforce_enums

class UnrecognizedValueException(ValueError):
'''
Raised by :meth:`EnumEnforcer.convert_enum` when a parameter is passed which is
not enumerated in the required enum type, and value validation is enabled.

Inherits from ``ValueError``.
'''


class EnumRequiredException(TypeError, ValueError):
'''
Raised by :meth:`EnumEnforcer.convert_enum` when a parameter is passed which is
not a member of the required enum type and enum-only enforcement is enabled.

Inherits from both ``TypeError`` and ``ValueError`` for backwards compatibility.
'''


class UnsuccessfulOrderException(ValueError):
'''
Expand Down
Loading