junkcode  Check-in [80e6e4b9ca]

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: 80e6e4b9ca33ada44a12aaf64e311dc888fbf6d81ed98e504cd3513a8b08413b
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
Unified Diff Ignore Whitespace Patch
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"]
}