Skip to content

Commit

Permalink
Casting improvements
Browse files Browse the repository at this point in the history
The primary purpose of this commit is to improve the casting
algorithm. Resources are now aware of their schema and can cast
based on this internal representation of the schema instead of guessing
the type based on `object`.

* Makes resources schema-aware
* Adds strict-mode
* Fixes generator issue with incorrectly labeled attribute types
* Add some tests to verify generated schemas
  • Loading branch information
bhelx committed Jun 18, 2019
1 parent 46d2523 commit f41d660
Show file tree
Hide file tree
Showing 9 changed files with 832 additions and 271 deletions.
8 changes: 8 additions & 0 deletions recurly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
name = "recurly"

import os
from os.path import dirname, basename, isfile
import glob

# Running in strict mode will throw exceptions
# when API responses don't line up with the client's expectations.
# The client's default behavior is to try to recover from these
# errors. This is used to catch bugs in testing.
# You do not want to enable this for production.
STRICT_MODE = os.getenv("RECURLY_STRICT_MODE", "FALSE").upper() == "TRUE"


class RecurlyError(Exception):
pass
Expand Down
106 changes: 77 additions & 29 deletions recurly/resource.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,95 @@
from pydoc import locate
import datetime
import recurly

# TODO - more resilient parsing
DT_FORMAT = "%Y-%m-%dT%H:%M:%SZ"


class Resource:
"""Class representing a server-side object in Recurly"""

# Allows us to override resource location
# Allows us to override resource location for testing
locator = lambda class_name: locate("recurly.resources.%s" % class_name)

@classmethod
def cast(cls, properties):
def cast(cls, properties, class_name=None):
"""Casts a dict of properties into a Recurly Resource"""

if "object" not in properties:
return properties

if (
properties["object"] == "list"
and "data" in properties
and "has_more" in properties
):
properties["data"] = [Resource.cast(i) for i in properties["data"]]
return Page(properties)

name_parts = properties["object"].split("_")
class_name = "".join(x.title() for x in name_parts)
if class_name is None and "object" in properties:
# If it's a Page, let's return that now
if (
properties["object"] == "list"
and "data" in properties
and "has_more" in properties
):
properties["data"] = [Resource.cast(i) for i in properties["data"]]
return Page(properties)

# If it's not a Page, we need to derive the class name
# from the "object" property. The class_name passed in should
# take precedence.
name_parts = properties["object"].split("_")
class_name = "".join(x.title() for x in name_parts)

klass = cls.locator(class_name)

# If we can't find a resource class, we should return
# the untyped properties dict. If in strict-mode, explode.
if klass is None:
return properties
if recurly.STRICT_MODE:
raise ValueError("Class could not be found for json: %s" % properties)
else:
return properties

del properties["object"]
resource = klass()
for k, v in properties.items():
if isinstance(v, dict):
properties[k] = Resource.cast(v)
elif isinstance(v, list):
for i in range(len(v)):
if isinstance(v[i], dict):
v[i] = Resource.cast(v[i])

return klass(properties)

def __init__(self, properties):
vars(self).update(properties)
# Skip "object" attributes
if k == "object":
continue

attr = None
attr_type = klass.schema.get(k)
if attr_type:
# if the value is None, let's set to none
# and skip the casting
if v is None:
attr = None

# if it's a plain type, use the type to cast it
elif type(attr_type) == type:
attr = attr_type(v)

# if it's a datetime, parse it
elif attr_type == datetime:
attr = datetime.datetime.strptime(v, DT_FORMAT)

# If the schema type a string, it's a reference
# to another resource
elif isinstance(attr_type, str) and isinstance(v, dict):
attr = Resource.cast(v, class_name=attr_type)

# If the schema type is a list of strings, it's a reference
# to a list of resources
elif (
isinstance(attr_type, list)
and isinstance(attr_type[0], str)
and isinstance(v, list)
):
attr = [Resource.cast(r, class_name=attr_type[0]) for r in v]

# We want to explode in strict mode because
# the schema doesn't know about this attribute. In production
# we will just set the attr to it's value or None
if recurly.STRICT_MODE and attr_type is None:
raise ValueError(
"%s could not find property %s in schema %s given value %s"
% (klass.__name__, k, klass.schema, v)
)
else:
setattr(resource, k, attr)

return resource


class Page(Resource):
Expand All @@ -59,4 +106,5 @@ class Page(Resource):
The list of data for this page. The data will be the requested type of Resource.
"""

pass
def __init__(self, properties):
vars(self).update(properties)
Loading

0 comments on commit f41d660

Please sign in to comment.