Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Add little Graph API project. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
80e6e4b9ca33ada44a12aaf64e311dc8 |
| User & Date: | jaccarmac 2017-12-28 08:47:51.072 |
Context
|
2017-12-29
| ||
| 19:14 | Add my little experiment. check-in: 8298ebc4dc user: jaccarmac tags: trunk | |
|
2017-12-28
| ||
| 08:47 | Add little Graph API project. check-in: 80e6e4b9ca user: jaccarmac tags: trunk | |
|
2017-12-27
| ||
| 04:30 | Add test of From behavior in Rust. check-in: d84628d47b user: jaccarmac tags: trunk | |
Changes
Changes to .fossil-settings/clean-glob.
1 | **/target/ | > | 1 2 | **/target/ **/*.pyc |
Added graph-api/README.md.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | Graph API Demo ============== Installation ------------ Using Conda is the easiest way to set up the environment for the project. `environment.yml` contains the definition, just run `conda env create -f environment.yml`. If Conda isn't available a Python 2.7 installation with the listed dependencies in recent versions (check `environment.yml` for exact versions) should work. Note that the code is not tested outside the included Conda environment. - dateutil - pytz Usage ----- Subclass from `graph_api.Node`. There are built-in conversion functions but you can also write your own. If deprecations are possible, handle DeprecatedAttributeErrors in client code. See tests for usage examples. Running tests ------------- `python test.py` Commentary/motivation --------------------- Graph APIs have some dusty corners which can cause issues with a dynamic object-oriented language like Python. This solution was designed to work around the gotchas I thought of while sticking to a Pythonic object model. 1. Types in the graph model clash with Python's type model a bit. Edges on the graph can connect an object (like a Facebook post) to a simple piece of data (like the text of the post) or to another graph node with more edges of its own (like comments on the post). In the Facebook API's case, this edges to other complex nodes might require another network request. This means that a property on the Python object representing an edge could either be a simple Python object like a string, integer, or list; or another graph edge object which requires a fetch to populate with data. This problem is solved by attaching a fetcher object to the node. The fetcher accepts a URI and populates the object with the data in its fields. Properties on the object can have URIs attached to them, and those properties will take care of the necessary fetching for themselves. On the other hand, specifying the precise type of properties is unnecessary so the API retains the dynamism of Python. 2. Facebook can change the names of their edges or the types of their nodes at any time. Varying types aren't really a problem for Python code. The lack of compile-time warnings can be annoying but a small set of integration tests against the real endpoints solves most problems. In many cases, code can be written polymorphically and the type changes invisible to consumers of the API. Name changes are a bigger problem given the lack of a compile phase. The mentioned integration tests can catch errors but are unhelpful for suggesting solutions to introduced failures. To solve this, each property can have an optional list of aliases. This allows simple renames to be ignored by client code, and with the next discussed feature provides a friendly deprecation path. 3. Javascript's missing property semantics, which many JSON APIs expect clients to respect, are entirely different from Python's. Javascript objects can contain uninitialized properties. They will have the Javascript `null` value. On the other hand, accessing a property which does not exist will return `undefined`. In Python, uninitialized properties are canonically `None` but throw an exception when they do not exist in their parent object at all. Thus, idiomatic Javascript code needs to handle both cases, as uninitialized properties are sometimes excluded from JSON for size and convenience. Deprecation is also pretty painless when clients are expected to treat missing data and null data the same way. This expectation is not true in Python. Thus, the library preserves the `None`-vs-exception dichotomy for Python but allows deprecation to be marked explicitly and throws a custom exception in that case. |
Added graph-api/environment.yml.
> > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | name: graph-api channels: - defaults dependencies: - ca-certificates=2017.08.26=h1d4fec5_0 - certifi=2017.11.5=py27h71e7faf_0 - dateutil=2.4.1=py27_0 - libedit=3.1=heed3624_0 - libffi=3.2.1=hd88cf55_4 - libgcc-ng=7.2.0=h7cc24e2_2 - libstdcxx-ng=7.2.0=h7a57d05_2 - ncurses=6.0=h9df7e31_2 - openssl=1.0.2n=hb7f436b_0 - pip=9.0.1=py27ha730c48_4 - python=2.7.14=h1571d57_29 - pytz=2017.3=py27h001bace_0 - readline=7.0=ha6073c6_4 - setuptools=36.5.0=py27h68b189e_0 - six=1.11.0=py27h5f960f1_1 - sqlite=3.20.1=hb898158_2 - tk=8.6.7=hc745277_3 - wheel=0.30.0=py27h2bc6bb2_1 - zlib=1.2.11=ha838bed_2 - pip: - python-dateutil==2.4.1 |
Added graph-api/graph_api.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import dateutil.parser
import json
class Node(object):
_deprecated = []
_aliases = {}
def __init__(self, raw):
node_obj = json.loads(raw)
self._prop_vals = {}
for prop in self._props:
self._prop_vals[prop] = self._props[prop].type(node_obj[prop])
def __getattr__(self, attr):
if attr in self._deprecated:
raise DeprecatedAttributeError(attr)
if attr in self._aliases:
attr = self._aliases[attr]
if attr not in self._props:
raise AttributeError(attr)
return self._prop_vals[attr]
class Prop(object):
def __init__(self, type):
self.type = type
class List(object):
def __init__(self, subtype):
self.subtype = subtype
def __call__(self, raw):
return [self.subtype(x) for x in raw]
class Fetchable(object):
def __init__(self, type, fetcher):
self.type = type
self.fetcher = fetcher
self.vals = {}
def __call__(self, raw):
if raw not in self.vals:
self.vals[raw] = self.type(self.fetcher.fetch(raw))
return self.vals[raw]
class DeprecatedAttributeError(AttributeError):
pass
def string(raw):
return raw
def int(raw):
return raw
def fb_datetime(raw):
return dateutil.parser.parse(raw)
|
Added graph-api/test.py.
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import unittest
import graph_api
import datetime
import pytz
class TestFileFetcher(object):
def fetch(self, uri):
with open('test_data/' + uri + '.json') as test_file:
return test_file.read()
tf_fetcher = TestFileFetcher()
class Comment(graph_api.Node):
_props = {
'message': graph_api.Prop(graph_api.string)
}
class Post(graph_api.Node):
_props = {
'message': graph_api.Prop(graph_api.string),
'associated_int': graph_api.Prop(graph_api.int),
'created_time': graph_api.Prop(graph_api.fb_datetime),
'comments': graph_api.Prop(graph_api.List(graph_api.Fetchable(Comment, tf_fetcher)))
}
_deprecated = ['deprecated_prop']
_aliases = {'contents': 'message'}
class TestFetch(unittest.TestCase):
post = Post(tf_fetcher.fetch('post/0'))
def test_string_property(self):
self.assertEqual(self.post.message, 'foo')
def test_int_property(self):
self.assertEqual(self.post.associated_int, 4)
def test_datetime_property(self):
self.assertEqual(self.post.created_time, datetime.datetime(2013, 1, 25, 0, 11, 2, 0, pytz.utc))
def test_fetchable_property(self):
self.assertEqual(self.post.comments[0].message, 'bar')
self.assertEqual(self.post.comments[1].message, 'quux')
class TestDeprecation(unittest.TestCase):
post = Post(tf_fetcher.fetch('post/0'))
def test_deprecation(self):
with self.assertRaises(graph_api.DeprecatedAttributeError):
self.post.deprecated_prop
class TestRename(unittest.TestCase):
post = Post(tf_fetcher.fetch('post/0'))
def test_old_prop(self):
self.assertEqual(self.post.contents, self.post.message)
if __name__ == '__main__':
unittest.main()
|
Added graph-api/test_data/comment/0.json.
> > > | 1 2 3 |
{
"message": "bar"
}
|
Added graph-api/test_data/comment/1.json.
> > > | 1 2 3 |
{
"message": "quux"
}
|
Added graph-api/test_data/post/0.json.
> > > > > > | 1 2 3 4 5 6 |
{
"message": "foo",
"associated_int": 4,
"created_time": "2013-01-25T00:11:02+0000",
"comments": ["comment/0", "comment/1"]
}
|