├── familysearch ├── tests │ ├── __init__.py │ ├── access_token.txt │ ├── request_token.txt │ ├── login.json │ ├── version.json │ ├── culture.json │ ├── version_list.json │ ├── culture_list.json │ ├── pedigree.json │ ├── place.json │ ├── identity_properties.json │ ├── pedigree_list.json │ ├── place_list.json │ ├── date.json │ ├── match.json │ ├── name.json │ ├── search.json │ ├── person1.json │ ├── persona.json │ ├── person2.json │ ├── date_list.json │ ├── name_list.json │ ├── person_list.json │ ├── persona_list.json │ ├── common.py │ ├── test_familysearch.py │ ├── test_authorities.py │ ├── test_familytree.py │ └── test_identity.py ├── enunciate │ ├── __init__.py │ ├── README │ └── identity.py ├── authorities_v1.py ├── familytree_v2.py ├── __init__.py └── identity_v2.py ├── MANIFEST.in ├── .gitignore ├── LICENSE ├── ROADMAP.rst ├── CHANGELOG.rst ├── setup.py ├── examples ├── login_web.py └── login.py └── README.rst /familysearch/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /familysearch/enunciate/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include familysearch/tests/*.json 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.html 3 | build 4 | dist 5 | MANIFEST 6 | -------------------------------------------------------------------------------- /familysearch/tests/access_token.txt: -------------------------------------------------------------------------------- 1 | oauth_token=FAKE_TOKEN&oauth_token_secret=FAKE_SECRET -------------------------------------------------------------------------------- /familysearch/tests/request_token.txt: -------------------------------------------------------------------------------- 1 | oauth_token=FAKE_TEMP_TOKEN&oauth_token_secret=FAKE_SECRET&oauth_callback_confirmed=true -------------------------------------------------------------------------------- /familysearch/tests/login.json: -------------------------------------------------------------------------------- 1 | {"session":{"id":"FAKE_SESSION_ID"},"version":"2.7.20110324.1454","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/version.json: -------------------------------------------------------------------------------- 1 | {"versions":[{"requestedId":"ABCD-123","id":"ABCD-123","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/culture.json: -------------------------------------------------------------------------------- 1 | {"names":null,"dates":null,"places":null,"cultures":[{"requestedId":"1","value":"North America","id":"1"}],"statusCode":200,"statusMessage":"OK","deprecated":null,"version":"1.8.20110324.1454"} 2 | -------------------------------------------------------------------------------- /familysearch/enunciate/README: -------------------------------------------------------------------------------- 1 | This directory should contain code generated by Enunciate to read and write the 2 | FamilySearch API's JSON. For now, since Enunciate does not yet generate Python 3 | code, this directory contains example code. 4 | -------------------------------------------------------------------------------- /familysearch/tests/version_list.json: -------------------------------------------------------------------------------- 1 | {"versions":[{"requestedId":"ABCD-123","id":"ABCD-123","version":"498216796184"},{"requestedId":"EFGH-456","id":"EFGH-456","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/culture_list.json: -------------------------------------------------------------------------------- 1 | {"names":null,"dates":null,"places":null,"cultures":[{"value":"North America","id":"1","requestedId":"1"},{"value":"Africa","id":"2","requestedId":"2"}],"version":"1.8.20110324.1454","statusCode":200,"statusMessage":"OK","deprecated":null} 2 | -------------------------------------------------------------------------------- /familysearch/tests/pedigree.json: -------------------------------------------------------------------------------- 1 | {"pedigrees":[{"requestedId":"ABCD-123","persons":[{"assertions":{"names":[{"value":{"forms":[{"fullText":"John Smith"}]}}],"genders":[{"value":{"type":"Male"}}]},"id":"ABCD-123","version":"498216796184"}],"id":"ABCD-123"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/place.json: -------------------------------------------------------------------------------- 1 | {"names":null,"dates":null,"places":{"count":1,"version":"4.6.0.2","place":[{"official":"London","normalized":["London, London, England"],"id":"5061446","type":"Capital Of A Political Entity","requestedId":null,"original":"London","culture":"4","iso":"GB-ENG"}]},"version":"1.8.20110324.1454","statusCode":200,"statusMessage":"OK","deprecated":null} 2 | -------------------------------------------------------------------------------- /familysearch/tests/identity_properties.json: -------------------------------------------------------------------------------- 1 | {"properties":[{"name":"user.max.ids","value":"10"},{"name":"request.token.url","value":"http://www.dev.usys.org:1/identity/v2/request_token"},{"name":"authorize.url","value":"http://www.dev.usys.org:2/identity/v2/authorize"},{"name":"access.token.url","value":"http://www.dev.usys.org:3/identity/v2/access_token"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/pedigree_list.json: -------------------------------------------------------------------------------- 1 | {"pedigrees":[{"requestedId":"ABCD-123","persons":[{"assertions":{"names":[{"value":{"forms":[{"fullText":"John Smith"}]}}],"genders":[{"value":{"type":"Male"}}]},"id":"ABCD-123","version":"498216796184"}],"id":"ABCD-123"},{"requestedId":"EFGH-456","persons":[{"assertions":{"names":[{"value":{"forms":[{"fullText":"Jane Smith"}]}}],"genders":[{"value":{"type":"Female"}}]},"id":"EFGH-456","version":"498216796184"}],"id":"EFGH-456"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/place_list.json: -------------------------------------------------------------------------------- 1 | {"names":null,"dates":null,"places":{"count":2,"version":"4.6.0.2","place":[{"official":"London","normalized":["London, London, England"],"id":"5061446","type":"Capital Of A Political Entity","requestedId":null,"original":"London","culture":"4","iso":"GB-ENG"},{"official":"Paris","normalized":["Paris, Ville-de-Paris, \u00cele-de-France, France"],"id":"5061509","type":"Capital Of A Political Entity","requestedId":null,"original":"Paris","culture":"6","iso":"FR-75"}]},"version":"1.8.20110324.1454","statusCode":200,"statusMessage":"OK","deprecated":null} 2 | -------------------------------------------------------------------------------- /familysearch/tests/date.json: -------------------------------------------------------------------------------- 1 | {"names":null,"dates":{"date":[{"normalized":"1 January 2000","earliest":{"normalized":"1 January 2000","numeric":"2000-01-01","astro":"2451545","original":null,"requested":null,"ambiguous":null,"valid":null},"latest":{"normalized":"1 January 2000","numeric":"2000-01-01","astro":"2451545","original":null,"requested":null,"ambiguous":null,"valid":null},"original":"1 Jan 2000","requested":"1 Jan 2000","ambiguous":false,"valid":true}],"version":"1.5.7.2","count":1},"places":null,"statusCode":200,"statusMessage":"OK","deprecated":null,"version":"1.8.20110324.1454"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/match.json: -------------------------------------------------------------------------------- 1 | {"matches":[{"id":"ABCD-123","count":1,"match":[{"score":0.5,"person":{"assertions":{"names":[{"value":{"forms":[{"fullText":"John Smith"}]}}],"genders":[{"value":{"type":"Male"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950"},"place":{"original":"London, England","normalized":{"value":"London, London, England","id":"5061446","version":"4.4.0.5m"},"selected":false}}}]},"minBirthYear":"1950","id":"ABCD-123"},"confidence":"High","id":"ABCD-123"}]}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/name.json: -------------------------------------------------------------------------------- 1 | {"names":{"count":1,"version":"4.6.0","name":[{"pieces":[{"predelimiters":"","value":"John","postdelimiters":" ","encodings":{"soundex":"J500","phondex":"\u02a4ohn","nysiis":"JAN"},"type":"Given","text":"John"},{"predelimiters":"","value":"Smith","postdelimiters":"","encodings":{"soundex":"S530","phondex":"smith","nysiis":"SNATH"},"type":"Family","text":"Smith"}],"fields":[{"type":"Given","text":"John"},{"type":"Family","text":"Smith"}],"original":"John Smith"}]},"dates":null,"places":null,"version":"1.8.20110324.1454","statusCode":200,"statusMessage":"OK","deprecated":null} 2 | -------------------------------------------------------------------------------- /familysearch/tests/search.json: -------------------------------------------------------------------------------- 1 | {"searches":[{"count":1,"close":500,"partial":500,"search":[{"score":5.0,"person":{"assertions":{"names":[{"value":{"forms":[{"fullText":"John Smith"}]}}],"genders":[{"value":{"type":"Male"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950"},"place":{"original":"London, England","normalized":{"value":"London, London, England","id":"5061446","version":"4.4.0.5m"},"selected":false}}}]},"minBirthYear":"1950","id":"ABCD-123"},"id":"ABCD-123"}],"contextId":"AQATMzMzOTc1MzY4ODc2Mjc0MDE1NAAAALEuWTBJAADtugA="}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 FamilySearch 2 | 3 | Licensed under the FamilySearch API License Agreement (the "License"); you may 4 | not use these files except in compliance with this License. This Sample Client 5 | Library is included as "Sample Code" under that license. 6 | 7 | You may obtain a copy of the License at 8 | http://devnet.familysearch.org/certification/affiliate-programs/familysearch-legal-agreements/APILicense.pdf 9 | 10 | Unless required by applicable law or agreed to in writing, software distributed 11 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 12 | CONDITIONS OF ANY KIND, either express or implied. 13 | 14 | See the License for the specific language governing permissions and limitations 15 | under the License. 16 | -------------------------------------------------------------------------------- /familysearch/tests/person1.json: -------------------------------------------------------------------------------- 1 | {"persons":[{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"John Smith","pieces":[{"predelimiters":"","value":"John","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Male"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"London, England","normalized":{"value":"London, London, England","id":"5061446","version":"4.4.0.5m"},"selected":false}}}]},"id":"ABCD-123","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/persona.json: -------------------------------------------------------------------------------- 1 | {"personas":[{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"John Smith","pieces":[{"predelimiters":"","value":"John","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Male"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"London, England","normalized":{"value":"London, London, England","id":"5061446","version":"4.4.0.5m"},"selected":false}}}]},"id":"ABCD-123","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/person2.json: -------------------------------------------------------------------------------- 1 | {"persons":[{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"Jane Smith","pieces":[{"predelimiters":"","value":"Jane","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Female"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"Paris, France","normalized":{"value":"Paris, \u00cele-de-France, France","id":"5061509","version":"4.4.0.5m"},"selected":false}}}]},"id":"EFGH-456","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /ROADMAP.rst: -------------------------------------------------------------------------------- 1 | ========================= 2 | python-fs-stack Roadmap 3 | ========================= 4 | 5 | 0.1 6 | --- 7 | 8 | - Log in with Basic Authentication 9 | - Log in with OAuth 10 | - Person Read 11 | - Pedigree Read 12 | - Person Search with GET parameters 13 | - Person Match with GET parameters 14 | 15 | 16 | 0.2 17 | --- 18 | 19 | - Authorities Read 20 | - Person Version Read 21 | - Persona Read 22 | - Regression tests 23 | 24 | 25 | Future 26 | ------ 27 | 28 | - Convenience methods to access FamilySearch's data model 29 | - Use persistent HTTP connections instead of a new connection for each request 30 | - Ensure Python 3 compatibility 31 | - Person Update 32 | - Relationship Read 33 | - Relationship Update 34 | - Person Combine 35 | - Persona Separate 36 | - User Read 37 | - User Update 38 | - Contributor Read 39 | - Discussions 40 | - Notes 41 | - Citations 42 | - Person Is Related 43 | - Cemetery Search 44 | -------------------------------------------------------------------------------- /familysearch/tests/date_list.json: -------------------------------------------------------------------------------- 1 | {"names":null,"dates":{"date":[{"normalized":"1 January 2000","earliest":{"normalized":"1 January 2000","numeric":"2000-01-01","astro":"2451545","original":null,"requested":null,"ambiguous":null,"valid":null},"latest":{"normalized":"1 January 2000","numeric":"2000-01-01","astro":"2451545","original":null,"requested":null,"ambiguous":null,"valid":null},"original":"1 Jan 2000","requested":"1 Jan 2000","ambiguous":false,"valid":true},{"normalized":"1 January 1900","earliest":{"normalized":"1 January 1900","numeric":"1900-01-01","astro":"2415021","original":null,"requested":null,"ambiguous":null,"valid":null},"latest":{"normalized":"1 January 1900","numeric":"1900-01-01","astro":"2415021","original":null,"requested":null,"ambiguous":null,"valid":null},"original":"1 Jan 1900","requested":"1 Jan 1900","ambiguous":false,"valid":true}],"version":"1.5.7.2","count":2},"places":null,"statusCode":200,"statusMessage":"OK","deprecated":null,"version":"1.8.20110324.1454"} 2 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | python-fs-stack Changelog 3 | =========================== 4 | 5 | 0.3 6 | --- 7 | 8 | 9 | 0.2 (8 Jun 2011) 10 | ---------------- 11 | 12 | * Supports Python 2.4 13 | * Supports Authorities requests (place, name, date, and culture) 14 | * Supports Person Version Read and Persona Read 15 | * Properly sends session ID on first request without needing to call session() 16 | * Search and Match return a single object instead of a one-item list 17 | * Supports pickling using any protocol (0-2) 18 | * Allows authorize() to supply arbitrary parameters 19 | * Handles HTTP 401 (Unauthorized) for Python 2.4 and 2.5 (GitHub issue 1) 20 | * Full regression test coverage (requires wsgi_intercept) 21 | * The login.py example reads the password more portably 22 | 23 | 24 | 0.1 (21 Jan 2011) 25 | ----------------- 26 | 27 | * Initial release 28 | * Supports Family Tree Read requests only (person, pedigree, search, match) 29 | * Returns parsed JSON 30 | * Requires Python 2.5 31 | -------------------------------------------------------------------------------- /familysearch/tests/name_list.json: -------------------------------------------------------------------------------- 1 | {"names":{"name":[{"pieces":[{"predelimiters":"","value":"John","postdelimiters":" ","encodings":{"soundex":"J500","phondex":"\u02a4ohn","nysiis":"JAN"},"type":"Given","text":"John"},{"predelimiters":"","value":"Smith","postdelimiters":"","encodings":{"soundex":"S530","phondex":"smith","nysiis":"SNATH"},"type":"Family","text":"Smith"}],"fields":[{"type":"Given","text":"John"},{"type":"Family","text":"Smith"}],"original":"John Smith"},{"pieces":[{"predelimiters":"","value":"Jane","postdelimiters":" ","encodings":{"soundex":"J500","phondex":"\u02a4an\u0259","nysiis":"JAN"},"type":"Given","text":"Jane"},{"predelimiters":"","value":"Smith","postdelimiters":"","encodings":{"soundex":"S530","phondex":"smith","nysiis":"SNATH"},"type":"Family","text":"Smith"}],"fields":[{"type":"Given","text":"Jane"},{"type":"Family","text":"Smith"}],"original":"Jane Smith"}],"version":"4.6.0","count":2},"dates":null,"places":null,"statusCode":200,"statusMessage":"OK","deprecated":null,"version":"1.8.20110324.1454"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/person_list.json: -------------------------------------------------------------------------------- 1 | {"persons":[{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"John Smith","pieces":[{"predelimiters":"","value":"John","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Male"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"London, England","normalized":{"value":"London, London, England","id":"5061446","version":"4.4.0.5m"},"selected":false}}}]},"id":"ABCD-123","version":"498216796184"},{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"Jane Smith","pieces":[{"predelimiters":"","value":"Jane","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Female"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"Paris, France","normalized":{"value":"Paris, \u00cele-de-France, France","id":"5061509","version":"4.4.0.5m"},"selected":false}}}]},"id":"EFGH-456","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/persona_list.json: -------------------------------------------------------------------------------- 1 | {"personas":[{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"John Smith","pieces":[{"predelimiters":"","value":"John","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Male"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"London, England","normalized":{"value":"London, London, England","id":"5061446","version":"4.4.0.5m"},"selected":false}}}]},"id":"ABCD-123","version":"498216796184"},{"assertions":{"names":[{"value":{"type":"Name","forms":[{"fullText":"Jane Smith","pieces":[{"predelimiters":"","value":"Jane","postdelimiters":" ","type":"Given"},{"predelimiters":"","value":"Smith","postdelimiters":"","type":"Family"}]}]}}],"genders":[{"value":{"type":"Female"}}],"events":[{"value":{"type":"Birth","date":{"original":"19500101","normalized":"1 January 1950","gedcom":"1 January 1950","numeric":"1950-01-01","astro":{"earliest":"2433283","latest":"2433283"},"selected":false},"place":{"original":"Paris, France","normalized":{"value":"Paris, \u00cele-de-France, France","id":"5061509","version":"4.4.0.5m"},"selected":false}}}]},"id":"EFGH-456","version":"498216796184"}],"version":"2.7.20110406.1514","statusCode":200,"statusMessage":"OK"} 2 | -------------------------------------------------------------------------------- /familysearch/tests/common.py: -------------------------------------------------------------------------------- 1 | """Common functions used by tests in multiple test suites""" 2 | 3 | import unittest 4 | import wsgi_intercept 5 | 6 | try: 7 | import pkg_resources 8 | def load_sample(filename): 9 | return pkg_resources.resource_string(__name__, filename) 10 | except ImportError: 11 | import os.path 12 | data_dir = os.path.dirname(__file__) 13 | def load_sample(filename): 14 | return open(os.path.join(data_dir, filename)).read() 15 | 16 | default_headers = {'Content-Type': 'application/json'} 17 | 18 | def add_request_intercept(response, out_environ=None, status='200 OK', 19 | host='www.dev.usys.org', port=80, 20 | headers=default_headers): 21 | """Globally install a request intercept returning the provided response.""" 22 | if out_environ is None: 23 | out_environ = {} 24 | def mock_app(environ, start_response): 25 | out_environ.update(environ) 26 | start_response(status, dict(headers).items()) 27 | return iter(response) 28 | wsgi_intercept.add_wsgi_intercept(host, port, lambda: mock_app) 29 | return out_environ 30 | 31 | def clear_request_intercpets(): 32 | """Remove all installed request intercepts.""" 33 | wsgi_intercept.remove_wsgi_intercept() 34 | 35 | if not hasattr(unittest.TestCase, 'assertIn'): 36 | unittest.TestCase.assertIn = lambda self, member, container, msg=None: unittest.TestCase.assertTrue(self, member in container, msg) 37 | if not hasattr(unittest.TestCase, 'assertNotIn'): 38 | unittest.TestCase.assertNotIn = lambda self, member, container, msg=None: unittest.TestCase.assertTrue(self, member not in container, msg) 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import familysearch 2 | try: 3 | # Setuptools or Distribute is required to support running `python setup.py test` 4 | from setuptools import setup 5 | except ImportError: 6 | # Distutils supports everything else; just run the test suite manually 7 | from distutils.core import setup 8 | 9 | setup( 10 | name='python-fs-stack', 11 | version=familysearch.__version__, 12 | description='Python wrapper for all FamilySearch APIs', 13 | long_description=open('README.rst').read(), 14 | url='http://pypi.python.org/pypi/python-fs-stack', 15 | author='Peter Henderson', 16 | author_email='peterhenderson@byu.net', 17 | license='FamilySearch API License Agreement ', 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Environment :: Console', 21 | 'Environment :: No Input/Output (Daemon)', 22 | 'Environment :: Web Environment', 23 | 'Intended Audience :: Developers', 24 | 'License :: Other/Proprietary License', 25 | 'Operating System :: OS Independent', 26 | 'Programming Language :: Python :: 2', 27 | 'Topic :: Internet :: WWW/HTTP', 28 | 'Topic :: Sociology :: Genealogy', 29 | 'Topic :: Software Development :: Libraries :: Python Modules', 30 | ], 31 | keywords=['FamilySearch', 'genealogy', 'family history', 'API', 'OAuth', 'REST', 'JSON'], 32 | packages=['familysearch', 'familysearch.enunciate', 'familysearch.tests'], 33 | scripts=['examples/login.py', 'examples/login_web.py'], 34 | test_suite='familysearch.tests', 35 | tests_require=['wsgi_intercept'], 36 | package_data={'familysearch.tests': ['*.json']}, 37 | ) 38 | -------------------------------------------------------------------------------- /examples/login_web.py: -------------------------------------------------------------------------------- 1 | # Sample OAuth authentication web app 2 | # By Peter Henderson 3 | 4 | # Requires: 5 | # - web.py 6 | 7 | import familysearch 8 | import web 9 | 10 | user_agent = 'LoginWebSample' 11 | developer_key = 'WCQY-7J1Q-GKVV-7DNM-SQ5M-9Q5H-JX3H-CMJK' 12 | familysearch_base_url = 'http://www.dev.usys.org' 13 | 14 | urls = ( 15 | '/', 'login', 16 | '/authorized', 'authorized', 17 | ) 18 | app = web.application(urls, globals()) 19 | 20 | 21 | class login(): 22 | 23 | def GET(self): 24 | callback_url = web.ctx.home + '/authorized' 25 | 26 | # Step 1: Get a request token. This is a temporary token that is used for 27 | # having the user authorize an access token and to sign the request to obtain 28 | # said access token. 29 | fs = familysearch.FamilySearch(user_agent, developer_key, base=familysearch_base_url) 30 | try: 31 | request_token = fs.request_token(callback_url) 32 | # Save these cookies so the web app doesn't have to manage state 33 | # with multiple FamilySearch proxy objects 34 | web.setcookie('request_token', request_token['oauth_token']) 35 | web.setcookie('request_token_secret', request_token['oauth_token_secret']) 36 | except: 37 | return 'Error obtaining request token.' 38 | 39 | # Step 2: Redirect to the provider's authorize page. 40 | raise web.found(fs.authorize()) 41 | 42 | 43 | class authorized(): 44 | 45 | def GET(self): 46 | # Step 3: Once the consumer has redirected the user back to the oauth_callback 47 | # URL you can request the access token the user has approved. You use the 48 | # request token to sign this request. After this is done you throw away the 49 | # request token and use the access token returned. You should store this 50 | # access token somewhere safe, like a database, for future use. 51 | cookies = web.cookies(request_token='', request_token_secret='') 52 | params = web.input(oauth_verifier='') 53 | 54 | fs = familysearch.FamilySearch(user_agent, developer_key, base=familysearch_base_url) 55 | try: 56 | fs.access_token(params.oauth_verifier, cookies.request_token, cookies.request_token_secret) 57 | return 'Your session is: %s' % fs.session_id 58 | except: 59 | return 'Error obtaining access token.' 60 | 61 | 62 | if __name__ == '__main__': 63 | app.run() 64 | -------------------------------------------------------------------------------- /familysearch/enunciate/identity.py: -------------------------------------------------------------------------------- 1 | try: 2 | import json 3 | except ImportError: 4 | import simplejson as json 5 | 6 | 7 | # Define `all` function for Python < 2.5 8 | if not hasattr(__builtins__, 'all'): 9 | def all(iterable): 10 | for element in iterable: 11 | if not element: 12 | return False 13 | return True 14 | 15 | 16 | def parse(input): 17 | """Parse specified file or string and return an Identity object created from it.""" 18 | if hasattr(input, "read"): 19 | data = json.load(input) 20 | else: 21 | data = json.loads(input) 22 | return Identity(data) 23 | 24 | 25 | class JSONBase: 26 | """Base class for all JSON-related objects""" 27 | def to_json(self): 28 | return json.dumps(self.to_json_dict()) 29 | 30 | def __repr__(self): 31 | return "%s(%s)" % (self.__class__.__name__, self.to_json_dict()) 32 | 33 | def __str__(self): 34 | return self.to_json() 35 | 36 | 37 | class FSDict(dict): 38 | """Convenience class to access FamilySearch-style property lists as dictionaries 39 | 40 | For example, 41 | [{"name": "key1", "value": "value1"}, {"name": "key2", "value": "value2"}] 42 | converts to 43 | {"key1": "value1", "key2": "value2"} 44 | 45 | """ 46 | 47 | def __init__(self, pairs=None): 48 | if isinstance(pairs, list) and all((isinstance(pair, dict) for pair in pairs)): 49 | dict.__init__(self) 50 | for pair in pairs: 51 | self[pair["name"]] = pair["value"] 52 | 53 | def to_json_array(self): 54 | return [{"name": key, "value": self[key]} for key in self] 55 | 56 | 57 | class Identity(JSONBase): 58 | def __init__(self, o): 59 | if "statusCode" in o: 60 | self.statusCode = o["statusCode"] 61 | if "statusMessage" in o: 62 | self.statusMessage = o["statusMessage"] 63 | if "version" in o: 64 | self.version = o["version"] 65 | if "properties" in o: 66 | self.properties = FSDict(o["properties"]) 67 | if "session" in o and o["session"]: 68 | self.session = Session(o["session"]) 69 | 70 | def to_json_dict(self): 71 | d = {} 72 | if hasattr(self, "statusCode"): 73 | d["statusCode"] = self.statusCode 74 | if hasattr(self, "statusMessage"): 75 | d["statusMessage"] = self.statusMessage 76 | if hasattr(self, "version"): 77 | d["version"] = self.version 78 | if hasattr(self, "properties"): 79 | d["properties"] = self.properties.to_json_array() 80 | if hasattr(self, "session"): 81 | d["session"] = self.session.to_json_dict() 82 | return d 83 | 84 | 85 | class Session(JSONBase): 86 | def __init__(self, o): 87 | if "id" in o: 88 | self.id = o["id"] 89 | if "type" in o: 90 | self.type = o["type"] 91 | 92 | def to_json_dict(self): 93 | d = {} 94 | if hasattr(self, "id"): 95 | d["id"] = self.id 96 | if hasattr(self, "type"): 97 | d["type"] = self.type 98 | return d 99 | -------------------------------------------------------------------------------- /familysearch/authorities_v1.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module implementing the Authorities version 1 API module 3 | 4 | Main class: AuthoritiesV1, meant to be mixed-in to the FamilySearch class 5 | """ 6 | 7 | try: 8 | import json 9 | except ImportError: 10 | import simplejson as json 11 | 12 | class AuthoritiesV1(object): 13 | 14 | """ 15 | A mix-in implementing the Authorities version 1 endpoints 16 | """ 17 | 18 | def __init__(self): 19 | """ 20 | Set up the URLs for this AuthoritiesV1 object. 21 | """ 22 | self.authorities_base = self.base + '/authorities/v1/' 23 | 24 | def place(self, place_id=None, options={}, **kw_options): 25 | """ 26 | Get an authoritative representation of a place or list of places from FamilySearch. 27 | """ 28 | if isinstance(place_id, list): 29 | place_id = ','.join(map(str, place_id)) 30 | url = self.authorities_base + 'place' 31 | if place_id: 32 | url = self._add_subpath(url, str(place_id)) 33 | if options or kw_options: 34 | url = self._add_query_params(url, options, **kw_options) 35 | response = json.load(self._request(url))['places']['place'] 36 | response = self._remove_nones(response) 37 | if len(response) == 1: 38 | return response[0] 39 | else: 40 | return response 41 | 42 | def name(self, name=None, options={}, **kw_options): 43 | """ 44 | Get an authoritative representation of a name or list of names from FamilySearch. 45 | """ 46 | url = self.authorities_base + 'name' 47 | if name: 48 | kw_options['name'] = name 49 | if options or kw_options: 50 | url = self._add_query_params(url, options, **kw_options) 51 | response = json.load(self._request(url))['names']['name'] 52 | response = self._remove_nones(response) 53 | if len(response) == 1: 54 | return response[0] 55 | else: 56 | return response 57 | 58 | def date(self, date=None, options={}, **kw_options): 59 | """ 60 | Get an authoritative representation of a date or list of dates from FamilySearch. 61 | """ 62 | url = self.authorities_base + 'date' 63 | if date: 64 | kw_options['date'] = date 65 | if options or kw_options: 66 | url = self._add_query_params(url, options, **kw_options) 67 | response = json.load(self._request(url))['dates']['date'] 68 | response = self._remove_nones(response) 69 | if len(response) == 1: 70 | return response[0] 71 | else: 72 | return response 73 | 74 | def culture(self, culture_id=None, options={}, **kw_options): 75 | """ 76 | Get an authoritative representation of a culture or list of cultures used by FamilySearch. 77 | """ 78 | if isinstance(culture_id, list): 79 | culture_id = ','.join(map(str, culture_id)) 80 | url = self.authorities_base + 'culture' 81 | if culture_id: 82 | url = self._add_subpath(url, str(culture_id)) 83 | if options or kw_options: 84 | url = self._add_query_params(url, options, **kw_options) 85 | response = json.load(self._request(url))['cultures'] 86 | response = self._remove_nones(response) 87 | if len(response) == 1: 88 | return response[0] 89 | else: 90 | return response 91 | 92 | from familysearch import FamilySearch 93 | FamilySearch.__bases__ += (AuthoritiesV1,) 94 | -------------------------------------------------------------------------------- /examples/login.py: -------------------------------------------------------------------------------- 1 | # Sample authentication console app 2 | # By Peter Henderson 3 | 4 | import BaseHTTPServer 5 | import getpass 6 | import urllib2 7 | import urlparse 8 | import webbrowser 9 | 10 | import familysearch 11 | 12 | user_agent = 'LoginSample' 13 | developer_key = 'WCQY-7J1Q-GKVV-7DNM-SQ5M-9Q5H-JX3H-CMJK' 14 | familysearch_base_url = 'http://www.dev.usys.org' 15 | 16 | 17 | def create_proxy(): 18 | return familysearch.FamilySearch(user_agent, developer_key, base=familysearch_base_url) 19 | 20 | 21 | def login_oauth(): 22 | """ 23 | Log into FamilySearch using OAuth. 24 | 25 | Get a request token, 26 | start a temporary local web server, 27 | open a browser to authorize the request token, 28 | exchange the authorized request token for an access token, 29 | and return the resulting authenticated FamilySearch proxy. 30 | 31 | Raise an exception if obtaining either the request token or access token fails. 32 | 33 | """ 34 | 35 | AUTHORIZED_URL = '/authorized' 36 | LOGIN_SUCCESS_HTML = """ 37 | 38 | 39 | Login success 40 | 41 | 42 | FamilySearch login successful. 43 | You may close this window and return to the application. 44 | 45 | 46 | """ 47 | LOGIN_FAILURE_HTML = """ 48 | 49 | 50 | Login failure 51 | 52 | 53 | FamilySearch login failed. 54 | Please close this window, return to the application, and try again. 55 | 56 | 57 | """ 58 | 59 | class OAuthLoginHandler(BaseHTTPServer.BaseHTTPRequestHandler): 60 | """Handle the callback request after the user authorizes the request token.""" 61 | 62 | def do_GET(self): 63 | url = urlparse.urlparse(self.path) 64 | if url.path == AUTHORIZED_URL: 65 | authorized_url_params = dict(urlparse.parse_qsl(url.query)) 66 | 67 | # Step 3: Once the consumer has redirected the user back to the oauth_callback 68 | # URL you can request the access token the user has approved. You use the 69 | # request token to sign this request. After this is done you throw away the 70 | # request token and use the access token returned. You should store this 71 | # access token somewhere safe, like a database, for future use. 72 | try: 73 | access_token = fs.access_token(authorized_url_params['oauth_verifier']) 74 | if 'oauth_token' in access_token: 75 | self.send_response(200) 76 | self.send_header('Content-type', 'text/html') 77 | self.end_headers() 78 | self.wfile.write(LOGIN_SUCCESS_HTML) 79 | return 80 | except urllib2.HTTPError: 81 | self.send_response(500) 82 | self.send_header('Content-type', 'text/html') 83 | self.end_headers() 84 | self.wfile.write(LOGIN_FAILURE_HTML) 85 | return 86 | self.send_error(500) 87 | 88 | def log_message(self, *args, **kwargs): 89 | """Override the log_message function to avoid printing the request to the terminal.""" 90 | pass 91 | 92 | 93 | fs = create_proxy() 94 | authorize_server = BaseHTTPServer.HTTPServer(('', 0), OAuthLoginHandler) 95 | port = authorize_server.server_port 96 | callback_url = 'http://localhost:%i%s' % (port, AUTHORIZED_URL) 97 | 98 | # Step 1: Get a request token. This is a temporary token that is used for 99 | # having the user authorize an access token and to sign the request to obtain 100 | # said access token. 101 | fs.request_token(callback_url) 102 | 103 | # Step 2: Open browser to the provider's authorize page. 104 | webbrowser.open(fs.authorize()) 105 | authorize_server.handle_request() 106 | if fs.logged_in: 107 | return fs 108 | else: 109 | raise Exception('Error obtaining access token.') 110 | 111 | 112 | def login_basic(): 113 | """ 114 | Log into FamilySearch using Basic Authentication. 115 | 116 | Raise urllib2.HTTPError(401) if credentials are invalid. 117 | 118 | """ 119 | username = raw_input('Username: ') 120 | # Hide keystrokes to avoid displaying the password (only works under POSIX) 121 | password = getpass.getpass('Password: ') 122 | fs = create_proxy() 123 | fs.login(username, password) 124 | return fs 125 | 126 | 127 | if __name__ == '__main__': 128 | fs = login_oauth() 129 | print 'Use session ID: %s' % fs.session_id 130 | -------------------------------------------------------------------------------- /familysearch/familytree_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module implementing the Family Tree version 2 API module 3 | 4 | Main class: FamilyTreeV2, meant to be mixed-in to the FamilySearch class 5 | """ 6 | 7 | try: 8 | import json 9 | except ImportError: 10 | import simplejson as json 11 | 12 | class FamilyTreeV2(object): 13 | 14 | """ 15 | A mix-in implementing the Family Tree version 2 endpoints 16 | """ 17 | 18 | def __init__(self): 19 | """ 20 | Set up the URLs for this FamilyTreeV2 object. 21 | """ 22 | self.familytree_base = self.base + '/familytree/v2/' 23 | 24 | def _remove_nones(self, arg): 25 | """ 26 | Remove all None values from a nested dict structure. 27 | 28 | This method exists because the FamilySearch API returns all attributes 29 | in a JSON response, with empty values set to null instead of being 30 | hidden from the response. 31 | 32 | """ 33 | if isinstance(arg, dict): 34 | return dict([(k, self._remove_nones(v)) for (k, v) in arg.iteritems() if v is not None]) 35 | elif isinstance(arg, list): 36 | return [self._remove_nones(i) for i in arg if i is not None] 37 | else: 38 | return arg 39 | 40 | def person(self, person_id=None, options={}, **kw_options): 41 | """ 42 | Get a representation of a person or list of persons from the family tree. 43 | """ 44 | if isinstance(person_id, list): 45 | person_id = ','.join(person_id) 46 | elif person_id == 'me': 47 | person_id = None 48 | url = self.familytree_base + 'person' 49 | if person_id: 50 | url = self._add_subpath(url, person_id) 51 | if options or kw_options: 52 | url = self._add_query_params(url, options, **kw_options) 53 | response = json.load(self._request(url))['persons'] 54 | response = self._remove_nones(response) 55 | if len(response) == 1: 56 | return response[0] 57 | else: 58 | return response 59 | 60 | def persona(self, persona_id, options={}, **kw_options): 61 | """ 62 | Get a representation of a persona or list of personas from the family tree. 63 | """ 64 | if isinstance(persona_id, list): 65 | persona_id = ','.join(persona_id) 66 | url = self.familytree_base + 'persona' 67 | if persona_id: 68 | url = self._add_subpath(url, persona_id) 69 | if options or kw_options: 70 | url = self._add_query_params(url, options, **kw_options) 71 | response = json.load(self._request(url))['personas'] 72 | response = self._remove_nones(response) 73 | if len(response) == 1: 74 | return response[0] 75 | else: 76 | return response 77 | 78 | def version(self, person_id): 79 | """ 80 | Read the latest version of a person or list of persons from the family tree. 81 | """ 82 | if isinstance(person_id, list): 83 | person_id = ','.join(person_id) 84 | url = self.familytree_base + 'version' 85 | if person_id: 86 | url = self._add_subpath(url, person_id) 87 | response = json.load(self._request(url))['versions'] 88 | response = self._remove_nones(response) 89 | if len(response) == 1: 90 | return response[0] 91 | else: 92 | return response 93 | 94 | def pedigree(self, person_id=None, options={}, **kw_options): 95 | """ 96 | Get a pedigree for the given person or list of persons from the family tree. 97 | """ 98 | if isinstance(person_id, list): 99 | person_id = ','.join(person_id) 100 | elif person_id == 'me': 101 | person_id = None 102 | url = self.familytree_base + 'pedigree' 103 | if person_id: 104 | url = self._add_subpath(url, person_id) 105 | if options or kw_options: 106 | url = self._add_query_params(url, options, **kw_options) 107 | response = json.load(self._request(url))['pedigrees'] 108 | response = self._remove_nones(response) 109 | if len(response) == 1: 110 | return response[0] 111 | else: 112 | return response 113 | 114 | def search(self, options={}, **kw_options): 115 | """ 116 | Search for persons in the family tree. 117 | 118 | This method only supports GET parameters, not an XML payload. 119 | """ 120 | url = self.familytree_base + 'search' 121 | if options or kw_options: 122 | url = self._add_query_params(url, options, **kw_options) 123 | response = json.load(self._request(url))['searches'] 124 | response = self._remove_nones(response) 125 | return response[0] 126 | 127 | def match(self, person_id=None, options={}, **kw_options): 128 | """ 129 | Search for possible duplicates in the family tree. 130 | 131 | This method only supports GET parameters, not an XML payload. 132 | """ 133 | if isinstance(person_id, list): 134 | person_id = ','.join(person_id) 135 | url = self.familytree_base + 'match' 136 | if person_id: 137 | url = self._add_subpath(url, person_id) 138 | if options or kw_options: 139 | url = self._add_query_params(url, options, **kw_options) 140 | response = json.load(self._request(url))['matches'] 141 | response = self._remove_nones(response) 142 | return response[0] 143 | 144 | from familysearch import FamilySearch 145 | FamilySearch.__bases__ += (FamilyTreeV2,) 146 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | python-fs-stack 3 | ================= 4 | 5 | ``python-fs-stack`` provides a Python package that simplifies access to the 6 | FamilySearch_ `REST-style API`_. 7 | 8 | .. _FamilySearch: https://new.familysearch.org/ 9 | .. _REST-style API: https://devnet.familysearch.org/docs/api 10 | 11 | :Home Page: 12 | http://pypi.python.org/pypi/python-fs-stack 13 | :Source Code: 14 | https://github.com/familysearch-devnet/python-fs-stack 15 | 16 | 17 | .. contents:: 18 | 19 | 20 | Dependencies 21 | ============ 22 | 23 | - Python 2.4 or later 24 | - simplejson_, if using Python older than 2.6 25 | - wsgi_intercept_ 0.5.0 or later (only required to run test suite) 26 | 27 | .. _simplejson: http://pypi.python.org/pypi/simplejson 28 | .. _wsgi_intercept: http://pypi.python.org/pypi/wsgi_intercept 29 | 30 | 31 | Installation 32 | ============ 33 | 34 | Using pip_:: 35 | 36 | pip install python-fs-stack 37 | 38 | or using easy_install_ (from setuptools_ or distribute_):: 39 | 40 | easy_install python-fs-stack 41 | 42 | or (after downloading manually):: 43 | 44 | python setup.py install 45 | 46 | .. _pip: http://www.pip-installer.org/ 47 | .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall 48 | .. _setuptools: http://pypi.python.org/pypi/setuptools 49 | .. _distribute: http://pypi.python.org/pypi/distribute 50 | 51 | 52 | Example Usage 53 | ============= 54 | 55 | First, import the FamilySearch class:: 56 | 57 | from familysearch import FamilySearch 58 | 59 | 60 | Authenticating with FamilySearch 61 | -------------------------------- 62 | 63 | ``python-fs-stack`` supports several ways of initiating a session with 64 | FamilySearch, including Basic Authentication, OAuth, and resuming a previous 65 | session. 66 | 67 | Log in immediately with Basic Authentication:: 68 | 69 | fs = FamilySearch('ClientApp/1.0', 'developer_key', 'username', 'password') 70 | 71 | Log in in a separate step with Basic Authentication:: 72 | 73 | fs = FamilySearch('ClientApp/1.0', 'developer_key') 74 | fs.login('username', 'password') 75 | 76 | Log in in two steps with Basic Authentication:: 77 | 78 | fs = FamilySearch('ClientApp/1.0', 'developer_key') 79 | fs.initialize() 80 | fs.authenticate('username', 'password') 81 | 82 | Log in with OAuth:: 83 | 84 | import webbrowser 85 | fs = FamilySearch('ClientApp/1.0', 'developer_key') 86 | fs.request_token() 87 | webbrowser.open(fs.authorize()) 88 | # [Enter username and password into browser window that opens] 89 | verifier = [verifier from resulting web page] 90 | fs.access_token(verifier) 91 | 92 | Resume a previous session:: 93 | 94 | fs = FamilySearch('ClientApp/1.0', 'developer_key', session='session_id') 95 | 96 | Use the production system instead of the reference system:: 97 | 98 | fs = FamilySearch('ClientApp/1.0', 'developer_key', base='https://api.familysearch.org') 99 | 100 | 101 | Maintaining and Ending a Session 102 | -------------------------------- 103 | 104 | Keep the current session active:: 105 | 106 | fs.session() 107 | 108 | Log out:: 109 | 110 | fs.logout() 111 | 112 | 113 | Accessing Family Tree Information 114 | --------------------------------- 115 | 116 | Print current user's family tree details:: 117 | 118 | print fs.person() 119 | 120 | To specify a person ID to retrieve, pass the ID as an argument:: 121 | 122 | print fs.person('ABCD-123') 123 | 124 | To print multiple family tree entries, pass a list of IDs as an argument. To 125 | pass additional parameters to the API, simply pass them as named arguments:: 126 | 127 | print fs.person(['ABCD-123', 'EFGH-456'], events='all', children='all') 128 | 129 | Print the latest version of a list of persons (this request is more lightweight 130 | than a full person request, so it supports more IDs at once):: 131 | 132 | for person in fs.version(['ABCD-123', 'EFGH-456']): 133 | print person['id'], person['version'] 134 | 135 | Print the contents of a persona:: 136 | 137 | print fs.persona('ABCD-123') 138 | 139 | Print current user's pedigree:: 140 | 141 | print fs.pedigree() 142 | 143 | Format the pedigree output more nicely:: 144 | 145 | import pprint 146 | pprint.pprint(fs.pedigree()) 147 | 148 | 149 | Searching for Persons in the Family Tree 150 | ---------------------------------------- 151 | 152 | Search for a male named John Smith:: 153 | 154 | results = fs.search(givenName='John', familyName='Smith', gender='Male', maxResults=10) 155 | 156 | Retrieve the second page of the previous search:: 157 | 158 | more_results = fs.search(contextId=results[0]['contextId'], maxResults=10, startIndex=10) 159 | 160 | Search for an exact match for John Smith (use an ``options`` dict to specify 161 | options with periods in their names):: 162 | 163 | results = fs.search(options={'givenName.exact': 'John', 'familyName.exact': 'Smith'}, gender='Male', maxResults=10) 164 | 165 | 166 | Searching for Possible Duplicates 167 | --------------------------------- 168 | 169 | Search for possible duplicates of a person:: 170 | 171 | matches = fs.match('ABCD-123') 172 | 173 | Compute match score between two persons:: 174 | 175 | match = fs.match('ABCD-123', id='EFGH-456') 176 | 177 | Search for possible duplicates matching specified parameters:: 178 | 179 | matches = fs.match(givenName='John', familyName='Smith', gender='Male', birthDate='1900', birthPlace='USA', deathDate='1950', deathPlace='USA') 180 | 181 | 182 | Standardizing Places, Names, and Dates 183 | -------------------------------------- 184 | 185 | Look up a place by name:: 186 | 187 | place = fs.place(place='paris') 188 | 189 | Look up a place by ID:: 190 | 191 | place = fs.place(5061509) 192 | 193 | Look up a list of places by ID:: 194 | 195 | places = fs.place([5061509, 5061446]) 196 | 197 | Look up a place by name, showing only the most likely result, returning results in another locale:: 198 | 199 | place = fs.place(place='germany', filter=True, locale='de') 200 | 201 | Standardize a name:: 202 | 203 | name = fs.name('John Smith') 204 | 205 | Standardize a list of names:: 206 | 207 | names = fs.name(['John Smith', 'Jane Doe']) 208 | 209 | Standardize a date:: 210 | 211 | date = fs.date('1-1-11') 212 | 213 | Standardize a list of dates:: 214 | 215 | dates = fs.date(['1-1-11', 'december 31 1999']) 216 | -------------------------------------------------------------------------------- /familysearch/tests/test_familysearch.py: -------------------------------------------------------------------------------- 1 | import familysearch 2 | import pickle 3 | import unittest 4 | import urllib2 5 | import wsgi_intercept.httplib_intercept 6 | try: 7 | import json 8 | except ImportError: 9 | import simplejson as json 10 | from common import * 11 | 12 | sample_person1 = load_sample('person1.json') 13 | sample_person2 = load_sample('person2.json') 14 | sample_login = load_sample('login.json') 15 | sample_identity_properties = load_sample('identity_properties.json') 16 | sample_request_token = load_sample('request_token.txt') 17 | 18 | 19 | class TestFamilySearch(unittest.TestCase): 20 | 21 | def setUp(self): 22 | self.longMessage = True 23 | self.agent = 'TEST_USER_AGENT' 24 | self.key = 'FAKE_DEV_KEY' 25 | self.session = 'FAKE_SESSION_ID' 26 | self.username = 'FAKE_USERNAME' 27 | self.password = 'FAKE_PASSWORD' 28 | self.cookie = 'FAKE_COOKIE=FAKE_VALUE' 29 | self.oauth_temp_token = 'FAKE_TEMP_TOKEN' 30 | self.oauth_secret = 'FAKE_SECRET' 31 | wsgi_intercept.httplib_intercept.install() 32 | 33 | def tearDown(self): 34 | clear_request_intercpets() 35 | wsgi_intercept.httplib_intercept.uninstall() 36 | 37 | def test_requires_user_agent(self): 38 | self.assertRaises(TypeError, familysearch.FamilySearch, key=self.key) 39 | 40 | def test_requires_dev_key(self): 41 | self.assertRaises(TypeError, familysearch.FamilySearch, agent=self.agent) 42 | 43 | def test_accepts_user_agent_and_dev_key(self): 44 | familysearch.FamilySearch(agent=self.agent, key=self.key) 45 | 46 | def test_changes_base(self): 47 | add_request_intercept(sample_person1, host='www.dev.usys.org', port=80) 48 | add_request_intercept(sample_person2, host='api.familysearch.org', port=443) 49 | fs_dev = familysearch.FamilySearch(self.agent, self.key) 50 | fs_prod = familysearch.FamilySearch(self.agent, self.key, base='https://api.familysearch.org') 51 | person1 = fs_dev.person() 52 | person2 = fs_prod.person() 53 | self.assertNotEqual(person1, person2, 'base argument failed to change base URL') 54 | self.assertEqual(person1['id'], json.loads(sample_person1)['persons'][0]['id'], 'wrong person returned from default base') 55 | self.assertEqual(person2['id'], json.loads(sample_person2)['persons'][0]['id'], 'wrong person returned from production base') 56 | 57 | def test_includes_user_agent(self): 58 | request_environ = add_request_intercept(sample_person1) 59 | fs = familysearch.FamilySearch(self.agent, self.key) 60 | fs.person() 61 | self.assertIn(self.agent, fs.agent, 'user agent not included in internal user agent') 62 | self.assertIn('HTTP_USER_AGENT', request_environ, 'user agent header not included in request') 63 | self.assertIn(self.agent, request_environ['HTTP_USER_AGENT'], 'user agent not included in user agent header') 64 | 65 | def test_restoring_session_sets_logged_in(self): 66 | fs = familysearch.FamilySearch(self.agent, self.key) 67 | self.assertFalse(fs.logged_in, 'should not be logged in by default') 68 | fs = familysearch.FamilySearch(self.agent, self.key, session=self.session) 69 | self.assertTrue(fs.logged_in, 'should be logged in after restoring session') 70 | 71 | def test_username_and_password_set_logged_in(self): 72 | add_request_intercept(sample_login) 73 | fs = familysearch.FamilySearch(self.agent, self.key, self.username, self.password) 74 | self.assertTrue(fs.logged_in, 'should be logged in after providing username and password') 75 | 76 | def test_requests_json_format(self): 77 | request_environ = add_request_intercept(sample_person1) 78 | fs = familysearch.FamilySearch(self.agent, self.key, session=self.session) 79 | fs.person() 80 | self.assertIn('QUERY_STRING', request_environ, 'query string not included in request') 81 | self.assertIn('dataFormat=application%2Fjson', request_environ['QUERY_STRING'], 'dataFormat not included in query string') 82 | 83 | def test_not_logged_in_if_error_401(self): 84 | add_request_intercept('', status='401 Unauthorized') 85 | fs = familysearch.FamilySearch(self.agent, self.key, session=self.session) 86 | self.assertTrue(fs.logged_in, 'should be logged in after restoring session') 87 | self.assertRaises(urllib2.HTTPError, fs.person) 88 | self.assertFalse(fs.logged_in, 'should not be logged in after receiving error 401') 89 | 90 | def test_passes_cookies_back(self): 91 | fs = familysearch.FamilySearch(self.agent, self.key) 92 | 93 | # First request sets a cookie 94 | headers = default_headers.copy() 95 | headers['Set-Cookie'] = self.cookie + '; Path=/' 96 | add_request_intercept(sample_login, headers=headers) 97 | fs.login(self.username, self.password) 98 | 99 | # Second request should receive the cookie back 100 | request_environ = add_request_intercept(sample_person1) 101 | fs.person() 102 | self.assertIn('HTTP_COOKIE', request_environ, 'cookie header not included in request') 103 | self.assertIn(self.cookie, request_environ['HTTP_COOKIE'], 'previously-set cookie not included in cookie header') 104 | 105 | def test_pickle_restores_logged_out_session(self): 106 | fs_logged_out = familysearch.FamilySearch(self.agent, self.key, base='https://api.familysearch.org') 107 | fs_logged_out_restored = pickle.loads(pickle.dumps(fs_logged_out)) 108 | self.assertEqual(fs_logged_out.agent, fs_logged_out_restored.agent, 'user agent not restored properly') 109 | self.assertEqual(fs_logged_out.key, fs_logged_out_restored.key, 'developer key not restored properly') 110 | self.assertEqual(fs_logged_out.session_id, fs_logged_out_restored.session_id, 'session ID not restored properly') 111 | self.assertEqual(fs_logged_out_restored.logged_in, False, 'logged-in flag not restored properly') 112 | self.assertEqual(fs_logged_out.base, fs_logged_out_restored.base, 'base URL not restored properly') 113 | 114 | def test_pickle_restores_logged_in_session(self): 115 | fs_logged_in = familysearch.FamilySearch(self.agent, self.key, base='https://api.familysearch.org', session=self.session) 116 | fs_logged_in_restored = pickle.loads(pickle.dumps(fs_logged_in)) 117 | self.assertEqual(fs_logged_in.agent, fs_logged_in_restored.agent, 'user agent not restored properly') 118 | self.assertEqual(fs_logged_in.key, fs_logged_in_restored.key, 'developer key not restored properly') 119 | self.assertEqual(fs_logged_in.session_id, fs_logged_in_restored.session_id, 'session ID not restored properly') 120 | self.assertEqual(fs_logged_in_restored.logged_in, True, 'logged-in flag not restored properly') 121 | self.assertEqual(fs_logged_in.base, fs_logged_in_restored.base, 'base URL not restored properly') 122 | 123 | def test_pickle_restores_oauth_temporary_credentials(self): 124 | add_request_intercept(sample_identity_properties) 125 | add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 126 | fs_oauth = familysearch.FamilySearch(self.agent, self.key) 127 | fs_oauth.request_token() 128 | fs_oauth_restored = pickle.loads(pickle.dumps(fs_oauth)) 129 | self.assertEqual(fs_oauth.agent, fs_oauth_restored.agent, 'user agent not restored properly') 130 | self.assertEqual(fs_oauth.key, fs_oauth_restored.key, 'developer key not restored properly') 131 | self.assertEqual(fs_oauth.session_id, fs_oauth_restored.session_id, 'session ID not restored properly') 132 | self.assertEqual(fs_oauth_restored.logged_in, False, 'logged-in flag not restored properly') 133 | self.assertEqual(fs_oauth.base, fs_oauth_restored.base, 'base URL not restored properly') 134 | self.assertIn(self.oauth_temp_token, fs_oauth_restored.oauth_secrets, 'OAuth temporary credentials identifier not restored properly') 135 | self.assertEqual(fs_oauth_restored.oauth_secrets[self.oauth_temp_token], self.oauth_secret, 'OAuth temporary credentials shared-secret not restored properly') 136 | 137 | 138 | if __name__ == '__main__': 139 | unittest.main() 140 | -------------------------------------------------------------------------------- /familysearch/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A library to interact with the FamilySearch API 3 | 4 | Licensed under the FamilySearch API License Agreement; 5 | see the included LICENSE file for details. 6 | 7 | Example usage: 8 | 9 | from familysearch import FamilySearch 10 | 11 | # Log in immediately with Basic Authentication 12 | fs = FamilySearch('ClientApp/1.0', 'developer_key', 'username', 'password') 13 | 14 | # Log in in a separate step with Basic Authentication 15 | fs = FamilySearch('ClientApp/1.0', 'developer_key') 16 | fs.login('username', 'password') 17 | 18 | # Log in in two steps with Basic Authentication 19 | fs = FamilySearch('ClientApp/1.0', 'developer_key') 20 | fs.initialize() 21 | fs.authenticate('username', 'password') 22 | 23 | # Resume a previous session 24 | fs = FamilySearch('ClientApp/1.0', 'developer_key', session='session_id') 25 | 26 | # Use the production system instead of the reference system 27 | fs = FamilySearch('ClientApp/1.0', 'developer_key', base='https://api.familysearch.org') 28 | 29 | # Log in with OAuth 30 | import webbrowser 31 | fs = FamilySearch('ClientApp/1.0', 'developer_key') 32 | fs.request_token() 33 | webbrowser.open(fs.authorize()) 34 | # [Enter username and password into browser window that opens] 35 | verifier = [verifier from resulting web page] 36 | fs.access_token(verifier) 37 | 38 | # Keep current session active 39 | fs.session() 40 | 41 | # Log out 42 | fs.logout() 43 | 44 | # Retrieve current user's family tree details 45 | me = fs.person() 46 | 47 | # Retrieve family tree details for a person or several persons 48 | person = fs.person('ABCD-123') 49 | persons = fs.person(['ABCD-123', 'EFGH-456']) 50 | 51 | # Retrieve family tree details for a list of persons, specifying parameters 52 | persons = fs.person(['ABCD-123', 'EFGH-456'], events='all', children='all') 53 | 54 | # Retrieve current user's pedigree 55 | pedigree = fs.pedigree() 56 | 57 | # Search for a male named John Smith and retrieve the second page 58 | results = fs.search(givenName='John', familyName='Smith', gender='Male', maxResults=10) 59 | more_results = fs.search(contextId=results[0]['contextId'], maxResults=10, startIndex=10) 60 | 61 | # Search for possible duplicates of a person 62 | matches = fs.match('ABCD-123') 63 | 64 | # Compute match score between two persons 65 | match = fs.match('ABCD-123', id='EFGH-456') 66 | 67 | # For more examples, see README.rst 68 | """ 69 | 70 | import urllib 71 | import urllib2 72 | import urlparse 73 | 74 | # Support Python < 2.6 75 | if not hasattr(urlparse, 'parse_qs'): 76 | import cgi 77 | urlparse.parse_qs = cgi.parse_qs 78 | 79 | __version__ = '0.3pre' 80 | 81 | 82 | class object(object): pass 83 | class FamilySearch(object): 84 | 85 | """ 86 | A FamilySearch API proxy 87 | 88 | The constructor must be called with a user-agent string and a developer key. 89 | A username, password, session ID, and base URL are all optional. 90 | 91 | Public methods: 92 | 93 | login -- log into FamilySearch with a username and password 94 | initialize -- create an unauthenticated session 95 | authenticate -- authenticate a session with a username and password 96 | logout -- log out of FamilySearch, terminating the current session 97 | session -- keep current session active 98 | request_token -- get an OAuth request token 99 | authorize -- construct OAuth authorization URL 100 | access_token -- get an OAuth access token (to complete the login process) 101 | 102 | person -- get a person or list of persons from the family tree 103 | persona -- get a persona or list of personas from the family tree 104 | version -- get the latest version number of a person from the family tree 105 | pedigree -- get the pedigree of a person or list of persons 106 | search -- search for persons in the family tree 107 | match -- search for possible duplicates in the family tree 108 | 109 | place -- standardize a place name 110 | name -- standardize a person name 111 | date -- standardize a date 112 | culture -- look up culture IDs 113 | 114 | Public attributes: 115 | 116 | logged_in -- flag indicating whether this proxy instance is logged in 117 | """ 118 | 119 | def __init__(self, agent, key, username=None, password=None, session=None, 120 | base='http://www.dev.usys.org'): 121 | """ 122 | Instantiate a FamilySearch proxy object. 123 | 124 | Keyword arguments: 125 | agent -- User-agent string to use for requests 126 | key -- FamilySearch developer key (optional if reusing an existing session ID) 127 | username (optional) 128 | password (optional) 129 | session (optional) -- existing session ID to reuse 130 | base (optional) -- base URL for the API; 131 | defaults to 'http://www.dev.usys.org' (the Reference System) 132 | """ 133 | self.agent = '%s Python-FS-Stack/%s' % (agent, __version__) 134 | self.key = key 135 | self.session_id = session 136 | self.base = base 137 | self.opener = urllib2.build_opener() 138 | 139 | for mixin in self.__class__.__bases__: 140 | mixin.__init__(self) 141 | 142 | if username and password: 143 | self.login(username, password) 144 | 145 | def __getstate__(self): 146 | """ 147 | Return a tuple containing the state necessary to pickle this instance. 148 | """ 149 | return ({'agent': ' '.join(self.agent.split(' ')[:-1]), 150 | 'key': self.key, 151 | 'session': self.session_id, 152 | 'base': self.base}, 153 | dict([(session, secret) 154 | for (session, secret) 155 | in self.oauth_secrets.iteritems() 156 | if session == self.session_id])) 157 | 158 | def __setstate__(self, state): 159 | """ 160 | Restore the saved state obtained from unpickling this instance. 161 | """ 162 | self.__init__(**state[0]) 163 | self.oauth_secrets = state[1] 164 | if self.session_id in self.oauth_secrets: 165 | self.logged_in = False 166 | 167 | def _request(self, url, data=None): 168 | """ 169 | Make a GET or a POST request to the FamilySearch API. 170 | 171 | Adds the User-Agent header and sets the response format to JSON. 172 | If the data argument is supplied, makes a POST request. 173 | Returns a file-like object representing the response. 174 | 175 | """ 176 | url = self._add_json_format(url) 177 | if self.logged_in and not self.cookies: 178 | # Add sessionId parameter to url if cookie is not set 179 | url = self._add_query_params(url, sessionId=self.session_id) 180 | request = urllib2.Request(url, data) 181 | request.add_header('User-Agent', self.agent) 182 | try: 183 | return self.opener.open(request) 184 | except urllib2.HTTPError, error: 185 | if error.code == 401: 186 | self.logged_in = False 187 | raise 188 | 189 | def _add_subpath(self, url, subpath): 190 | """ 191 | Add a subpath to the path component of the given URL. 192 | 193 | For example, adding sub to http://example.com/path?query 194 | becomes http://example.com/path/sub?query. 195 | 196 | """ 197 | parts = urlparse.urlsplit(url) 198 | path = parts[2] + '/' + subpath 199 | return urlparse.urlunsplit((parts[0], parts[1], path, parts[3], parts[4])) 200 | 201 | def _add_query_params(self, url, params={}, **kw_params): 202 | """ 203 | Add the specified query parameters to the given URL. 204 | 205 | Parameters can be passed either as a dictionary or as keyword arguments. 206 | 207 | """ 208 | parts = urlparse.urlsplit(url) 209 | query_parts = urlparse.parse_qs(parts[3]) 210 | query_parts.update(params) 211 | query_parts.update(kw_params) 212 | query = urllib.urlencode(query_parts, True) 213 | return urlparse.urlunsplit((parts[0], parts[1], parts[2], query, parts[4])) 214 | 215 | def _add_json_format(self, url): 216 | """ 217 | Add dataFormat=application/json to the query string of the given URL. 218 | """ 219 | return self._add_query_params(url, dataFormat='application/json') 220 | 221 | import identity_v2 222 | import familytree_v2 223 | import authorities_v1 224 | -------------------------------------------------------------------------------- /familysearch/identity_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | A module implementing the Identity version 2 API module 3 | 4 | Main class: IdentityV2, meant to be mixed-in to the FamilySearch class 5 | """ 6 | 7 | import random 8 | import time 9 | import urllib 10 | import urllib2 11 | import urlparse 12 | 13 | # Support Python < 2.6 14 | if not hasattr(urlparse, 'parse_qsl'): 15 | import cgi 16 | urlparse.parse_qsl = cgi.parse_qsl 17 | 18 | from enunciate import identity 19 | 20 | class IdentityV2(object): 21 | 22 | """ 23 | A mix-in implementing the Identity version 2 endpoints 24 | """ 25 | 26 | def __init__(self): 27 | """ 28 | Set up the URLs for this IdentityV2 object. 29 | """ 30 | self.identity_base = self.base + '/identity/v2/' 31 | 32 | self.oauth_secrets = dict() 33 | 34 | # Assume logged_in if session_id is set 35 | self.logged_in = bool(self.session_id) 36 | 37 | cookie_handler = urllib2.HTTPCookieProcessor() 38 | self.cookies = cookie_handler.cookiejar 39 | self.opener = urllib2.build_opener(cookie_handler) 40 | 41 | @property 42 | def identity_properties(self): 43 | """ 44 | Retrieve and cache the Identity version 2 module's properties. 45 | """ 46 | url = self.identity_base + 'properties' 47 | if not hasattr(self, '_identity_properties'): 48 | self._identity_properties = identity.parse(self._request(url)).properties 49 | return self._identity_properties 50 | 51 | def login(self, username, password): 52 | """ 53 | Log into FamilySearch using Basic Authentication. 54 | 55 | Web applications must use OAuth. 56 | 57 | """ 58 | self.logged_in = False 59 | self.cookies.clear() 60 | url = self.identity_base + 'login' 61 | credentials = urllib.urlencode({'username': username, 62 | 'password': password, 63 | 'key': self.key}) 64 | self.session_id = identity.parse(self._request(url, credentials)).session.id 65 | self.logged_in = True 66 | return self.session_id 67 | 68 | def initialize(self): 69 | """ 70 | Initialize a FamilySearch session using Basic Authentication. 71 | 72 | This creates an unauthenticated session and should be followed by an 73 | authenticate call. Web applications must use OAuth. 74 | 75 | """ 76 | self.logged_in = False 77 | self.cookies.clear() 78 | url = self.identity_base + 'initialize' 79 | key = urllib.urlencode({'key': self.key}) 80 | self.session_id = identity.parse(self._request(url, key)).session.id 81 | return self.session_id 82 | 83 | def authenticate(self, username, password): 84 | """ 85 | Authenticate a FamilySearch session using Basic Authentication. 86 | 87 | This should follow an initialize call. Web applications must use OAuth. 88 | 89 | """ 90 | url = self.identity_base + 'authenticate' 91 | credentials = {'username': username, 'password': password} 92 | if not self.cookies and self.session_id: 93 | # Set sessionId parameter if the session ID is not set in a cookie 94 | credentials['sessionId'] = self.session_id 95 | credentials = urllib.urlencode(credentials) 96 | self.session_id = identity.parse(self._request(url, credentials)).session.id 97 | self.logged_in = True 98 | return self.session_id 99 | 100 | def logout(self): 101 | """ 102 | Log the current session out of FamilySearch. 103 | """ 104 | self.logged_in = False 105 | url = self.identity_base + 'logout' 106 | self._request(url) 107 | self.session_id = None 108 | self.cookies.clear() 109 | 110 | def session(self): 111 | """ 112 | Keep the current session in an active state by sending an empty request. 113 | 114 | Calling this method is an easy way to turn a sessionId query parameter 115 | into a cookie without doing anything else. 116 | 117 | """ 118 | url = self.identity_base + 'session' 119 | self.session_id = identity.parse(self._request(url)).session.id 120 | self.logged_in = True 121 | return self.session_id 122 | 123 | def request_token(self, callback_url='oob'): 124 | """ 125 | Get a request token for step 1 of the OAuth login process. 126 | 127 | Returns a dictionary containing the OAuth response and stores the token 128 | and token secret, which are needed to get an access token (step 3). 129 | 130 | """ 131 | self.logged_in = False 132 | self.cookies.clear() 133 | url = self.identity_properties['request.token.url'] 134 | oauth_response = self._oauth_request(url, oauth_callback=callback_url) 135 | response = dict(urlparse.parse_qsl(oauth_response.read())) 136 | self.session_id = response['oauth_token'] 137 | self.oauth_secrets[response['oauth_token']] = response['oauth_token_secret'] 138 | return response 139 | 140 | def authorize(self, request_token=None, options={}, **kw_options): 141 | """ 142 | Construct and return the User Authorization URL for step 2 of the OAuth login process. 143 | 144 | This URL should be loaded into the user's browser. It is the 145 | application's responsibility to receive the OAuth verifier from the 146 | callback URL (such as by running an HTTP server) or to provide a means 147 | for the user to enter the verifier into the application. 148 | 149 | """ 150 | if not request_token: 151 | if self.session_id and self.session_id in self.oauth_secrets: 152 | # Use current session ID for oauth_token if it is set 153 | request_token = self.session_id 154 | else: 155 | # Otherwise, get a new request token and use it 156 | request_token = self.request_token()['oauth_token'] 157 | # Add sessionId parameter to authorize.url 158 | url = self.identity_properties['authorize.url'] 159 | url = self._add_query_params(url, sessionId=request_token) 160 | if options or kw_options: 161 | url = self._add_query_params(url, options, **kw_options) 162 | return url 163 | 164 | def access_token(self, verifier, request_token=None, token_secret=None): 165 | """ 166 | Get an access token (session ID) to complete step 3 of the OAuth login process. 167 | 168 | Returns a dictionary containing the OAuth response and stores the token 169 | as the session ID to be used by future requests. 170 | 171 | """ 172 | if not request_token and self.session_id: 173 | # Use current session ID for oauth_token if it is set 174 | request_token = self.session_id 175 | if not token_secret and request_token in self.oauth_secrets: 176 | # Use saved secret for oauth_token_secret if it is set 177 | token_secret = self.oauth_secrets[request_token] 178 | url = self.identity_properties['access.token.url'] 179 | oauth_response = self._oauth_request(url, token_secret, 180 | oauth_token=request_token, 181 | oauth_verifier=verifier) 182 | response = dict(urlparse.parse_qsl(oauth_response.read())) 183 | if self.session_id in self.oauth_secrets: 184 | del self.oauth_secrets[self.session_id] 185 | self.session_id = response['oauth_token'] 186 | self.logged_in = True 187 | return response 188 | 189 | def _oauth_request(self, url, token_secret='', params={}, **kw_params): 190 | """ 191 | Make an OAuth request. 192 | 193 | This function only supports the PLAINTEXT signature method. 194 | Returns a file-like object representing the response. 195 | 196 | Keyword arguments: 197 | url -- the URL to request 198 | token_secret (optional) -- the request token secret, if requesting an 199 | access token (defaults to empty) 200 | params -- a dictionary of parameters to add to the request, such as 201 | oauth_callback, oauth_token, or oauth_verifier 202 | 203 | Additional parameters can be passed either as a dictionary or as 204 | keyword arguments. 205 | 206 | """ 207 | oauth_params = dict(params) 208 | oauth_params.update(kw_params) 209 | oauth_params.update({ 210 | 'oauth_consumer_key': self.key, 211 | 'oauth_nonce': str(random.randint(0, 99999999)), 212 | 'oauth_signature_method': 'PLAINTEXT', 213 | 'oauth_signature': '%s&%s' % ('', token_secret), 214 | 'oauth_timestamp': str(int(time.time())), 215 | }) 216 | data = urllib.urlencode(oauth_params, True) 217 | request = urllib2.Request(url, data) 218 | request.add_header('User-Agent', self.agent) 219 | try: 220 | return self.opener.open(request) 221 | except urllib2.HTTPError, error: 222 | if error.code == 401: 223 | self.logged_in = False 224 | raise 225 | 226 | from familysearch import FamilySearch 227 | FamilySearch.__bases__ += (IdentityV2,) 228 | -------------------------------------------------------------------------------- /familysearch/tests/test_authorities.py: -------------------------------------------------------------------------------- 1 | import familysearch 2 | import unittest 3 | import wsgi_intercept.httplib_intercept 4 | try: 5 | import json 6 | except ImportError: 7 | import simplejson as json 8 | from common import * 9 | 10 | sample_place = load_sample('place.json') 11 | sample_place_list = load_sample('place_list.json') 12 | sample_name = load_sample('name.json') 13 | sample_name_list = load_sample('name_list.json') 14 | sample_date = load_sample('date.json') 15 | sample_date_list = load_sample('date_list.json') 16 | sample_culture = load_sample('culture.json') 17 | sample_culture_list = load_sample('culture_list.json') 18 | 19 | 20 | class TestAuthorities(unittest.TestCase): 21 | 22 | def setUp(self): 23 | self.longMessage = True 24 | self.agent = 'TEST_USER_AGENT' 25 | self.key = 'FAKE_DEV_KEY' 26 | self.session = 'FAKE_SESSION_ID' 27 | self.id = 'FAKE_ID' 28 | self.id2 = 'FAKE_ID_2' 29 | wsgi_intercept.httplib_intercept.install() 30 | self.fs = familysearch.FamilySearch(self.agent, self.key, session=self.session) 31 | 32 | def tearDown(self): 33 | clear_request_intercpets() 34 | wsgi_intercept.httplib_intercept.uninstall() 35 | 36 | 37 | class TestAuthoritiesPlace(TestAuthorities): 38 | 39 | def test_accepts_single_place(self): 40 | request_environ = add_request_intercept(sample_place) 41 | self.fs.place(self.id) 42 | self.assertTrue(request_environ['PATH_INFO'].endswith('place/' + self.id), 'incorrect place request with single place ID') 43 | 44 | def test_accepts_list_of_places(self): 45 | request_environ = add_request_intercept(sample_place_list) 46 | self.fs.place([self.id, self.id2]) 47 | self.assertTrue(request_environ['PATH_INFO'].endswith('place/' + self.id + ',' + self.id2), 'incorrect place request with list of place IDs') 48 | 49 | def test_accepts_list_of_place_names_in_kwargs(self): 50 | request_environ = add_request_intercept(sample_place_list) 51 | self.fs.place(place=[self.id, self.id2]) 52 | self.assertTrue(request_environ['PATH_INFO'].endswith('place'), 'incorrect place request with list of place names') 53 | self.assertIn('place=' + self.id, request_environ['QUERY_STRING'], 'one of multiple place names not included') 54 | self.assertIn('place=' + self.id2, request_environ['QUERY_STRING'], 'one of multiple place names not included') 55 | 56 | def test_accepts_list_of_place_names_in_dict(self): 57 | request_environ = add_request_intercept(sample_place_list) 58 | self.fs.place(options={'place': [self.id, self.id2]}) 59 | self.assertTrue(request_environ['PATH_INFO'].endswith('place'), 'incorrect place request with list of place names') 60 | self.assertIn('place=' + self.id, request_environ['QUERY_STRING'], 'one of multiple place names not included') 61 | self.assertIn('place=' + self.id2, request_environ['QUERY_STRING'], 'one of multiple place names not included') 62 | 63 | def test_single_returns_single(self): 64 | add_request_intercept(sample_place) 65 | place = self.fs.place(self.id) 66 | self.assertEqual(type(place), dict, 'single place response is wrong type') 67 | 68 | def test_list_returns_list(self): 69 | add_request_intercept(sample_place_list) 70 | place_list = self.fs.place([self.id, self.id2]) 71 | self.assertEqual(type(place_list), list, 'multiple place response is not a list') 72 | self.assertEqual(len(place_list), 2, 'multiple place response has wrong length') 73 | 74 | def test_adds_one_query_param_from_kwargs(self): 75 | request_environ = add_request_intercept(sample_place) 76 | self.fs.place(place='London') 77 | self.assertIn('place=London', request_environ['QUERY_STRING'], 'single query parameter not included') 78 | 79 | def test_adds_multiple_query_params_from_kwargs(self): 80 | request_environ = add_request_intercept(sample_place) 81 | self.fs.place(place='London', view='full') 82 | self.assertIn('place=London', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 83 | self.assertIn('view=full', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 84 | 85 | def test_adds_one_query_param_from_dict(self): 86 | request_environ = add_request_intercept(sample_place) 87 | self.fs.place(options={'place': 'London'}) 88 | self.assertIn('place=London', request_environ['QUERY_STRING'], 'single query parameter not included') 89 | 90 | def test_adds_multiple_query_params_from_dict(self): 91 | request_environ = add_request_intercept(sample_place) 92 | self.fs.place(options={'place': 'London', 'view': 'full'}) 93 | self.assertIn('place=London', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 94 | self.assertIn('view=full', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 95 | 96 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 97 | request_environ = add_request_intercept(sample_place) 98 | self.fs.place(place='London', view='full', options={'variants': 'true', 'children': 'true'}) 99 | self.assertIn('place=London', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 100 | self.assertIn('view=full', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 101 | self.assertIn('variants=true', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 102 | self.assertIn('children=true', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 103 | 104 | 105 | class TestAuthoritiesName(TestAuthorities): 106 | 107 | def test_accepts_list_of_names_in_kwargs(self): 108 | request_environ = add_request_intercept(sample_name_list) 109 | self.fs.name(name=[self.id, self.id2]) 110 | self.assertTrue(request_environ['PATH_INFO'].endswith('name'), 'incorrect name request with list of names') 111 | self.assertIn('name=' + self.id, request_environ['QUERY_STRING'], 'one of multiple names not included') 112 | self.assertIn('name=' + self.id2, request_environ['QUERY_STRING'], 'one of multiple names not included') 113 | 114 | def test_accepts_list_of_names_in_dict(self): 115 | request_environ = add_request_intercept(sample_name_list) 116 | self.fs.name(options={'name': [self.id, self.id2]}) 117 | self.assertTrue(request_environ['PATH_INFO'].endswith('name'), 'incorrect name request with list of names') 118 | self.assertIn('name=' + self.id, request_environ['QUERY_STRING'], 'one of multiple names not included') 119 | self.assertIn('name=' + self.id2, request_environ['QUERY_STRING'], 'one of multiple names not included') 120 | 121 | def test_single_returns_single(self): 122 | add_request_intercept(sample_name) 123 | name = self.fs.name(name=self.id) 124 | self.assertEqual(type(name), dict, 'single name response is wrong type') 125 | 126 | def test_list_returns_list(self): 127 | add_request_intercept(sample_name_list) 128 | name_list = self.fs.name(name=[self.id, self.id2]) 129 | self.assertEqual(type(name_list), list, 'multiple name response is not a list') 130 | self.assertEqual(len(name_list), 2, 'multiple name response has wrong length') 131 | 132 | def test_adds_one_query_param_from_kwargs(self): 133 | request_environ = add_request_intercept(sample_name) 134 | self.fs.name(name='John Smith') 135 | self.assertIn('name=John+Smith', request_environ['QUERY_STRING'], 'single query parameter not included') 136 | 137 | def test_adds_multiple_query_params_from_kwargs(self): 138 | request_environ = add_request_intercept(sample_name) 139 | self.fs.name(name='John Smith', variants='false') 140 | self.assertIn('name=John+Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 141 | self.assertIn('variants=false', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 142 | 143 | def test_adds_one_query_param_from_dict(self): 144 | request_environ = add_request_intercept(sample_name) 145 | self.fs.name(options={'name': 'John Smith'}) 146 | self.assertIn('name=John+Smith', request_environ['QUERY_STRING'], 'single query parameter not included') 147 | 148 | def test_adds_multiple_query_params_from_dict(self): 149 | request_environ = add_request_intercept(sample_name) 150 | self.fs.name(options={'name': 'John Smith', 'variants': 'false'}) 151 | self.assertIn('name=John+Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 152 | self.assertIn('variants=false', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 153 | 154 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 155 | request_environ = add_request_intercept(sample_name) 156 | self.fs.name(name='John Smith', culture=1, options={'variants': 'true', 'test': 'test'}) 157 | self.assertIn('name=John+Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 158 | self.assertIn('culture=1', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 159 | self.assertIn('variants=true', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 160 | self.assertIn('test=test', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 161 | 162 | 163 | class TestAuthoritiesDate(TestAuthorities): 164 | 165 | def test_accepts_list_of_dates_in_kwargs(self): 166 | request_environ = add_request_intercept(sample_date_list) 167 | self.fs.date(date=[self.id, self.id2]) 168 | self.assertTrue(request_environ['PATH_INFO'].endswith('date'), 'incorrect date request with list of dates') 169 | self.assertIn('date=' + self.id, request_environ['QUERY_STRING'], 'one of multiple dates not included') 170 | self.assertIn('date=' + self.id2, request_environ['QUERY_STRING'], 'one of multiple dates not included') 171 | 172 | def test_accepts_list_of_dates_in_dict(self): 173 | request_environ = add_request_intercept(sample_date_list) 174 | self.fs.date(options={'date': [self.id, self.id2]}) 175 | self.assertTrue(request_environ['PATH_INFO'].endswith('date'), 'incorrect date request with list of dates') 176 | self.assertIn('date=' + self.id, request_environ['QUERY_STRING'], 'one of multiple dates not included') 177 | self.assertIn('date=' + self.id2, request_environ['QUERY_STRING'], 'one of multiple dates not included') 178 | 179 | def test_single_returns_single(self): 180 | add_request_intercept(sample_date) 181 | date = self.fs.date(date=self.id) 182 | self.assertEqual(type(date), dict, 'single date response is wrong type') 183 | 184 | def test_list_returns_list(self): 185 | add_request_intercept(sample_date_list) 186 | date_list = self.fs.date(date=[self.id, self.id2]) 187 | self.assertEqual(type(date_list), list, 'multiple date response is not a list') 188 | self.assertEqual(len(date_list), 2, 'multiple date response has wrong length') 189 | 190 | def test_adds_one_query_param_from_kwargs(self): 191 | request_environ = add_request_intercept(sample_date) 192 | self.fs.date(date='1 Jan 2000') 193 | self.assertIn('date=1+Jan+2000', request_environ['QUERY_STRING'], 'single query parameter not included') 194 | 195 | def test_adds_multiple_query_params_from_kwargs(self): 196 | request_environ = add_request_intercept(sample_date) 197 | self.fs.date(date='1 Jan 2000', astro=2451545) 198 | self.assertIn('date=1+Jan+2000', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 199 | self.assertIn('astro=2451545', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 200 | 201 | def test_adds_one_query_param_from_dict(self): 202 | request_environ = add_request_intercept(sample_date) 203 | self.fs.date(options={'date': '1 Jan 2000'}) 204 | self.assertIn('date=1+Jan+2000', request_environ['QUERY_STRING'], 'single query parameter not included') 205 | 206 | def test_adds_multiple_query_params_from_dict(self): 207 | request_environ = add_request_intercept(sample_date) 208 | self.fs.date(options={'date': '1 Jan 2000', 'astro': 2451545}) 209 | self.assertIn('date=1+Jan+2000', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 210 | self.assertIn('astro=2451545', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 211 | 212 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 213 | request_environ = add_request_intercept(sample_date) 214 | self.fs.date(date='1 Jan 2000', astro=2451545, options={'locale': 'en', 'test': 'test'}) 215 | self.assertIn('date=1+Jan+2000', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 216 | self.assertIn('astro=2451545', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 217 | self.assertIn('locale=en', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 218 | self.assertIn('test=test', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 219 | 220 | 221 | class TestAuthoritiesCulture(TestAuthorities): 222 | 223 | def test_accepts_no_arguments(self): 224 | request_environ = add_request_intercept(sample_culture) 225 | self.fs.culture() 226 | self.assertTrue(request_environ['PATH_INFO'].endswith('culture'), 'culture request failed without a culture ID') 227 | 228 | def test_accepts_single_culture(self): 229 | request_environ = add_request_intercept(sample_culture) 230 | self.fs.culture(self.id) 231 | self.assertTrue(request_environ['PATH_INFO'].endswith('culture/' + self.id), 'incorrect culture request with single culture ID') 232 | 233 | def test_accepts_list_of_cultures(self): 234 | request_environ = add_request_intercept(sample_culture_list) 235 | self.fs.culture([self.id, self.id2]) 236 | self.assertTrue(request_environ['PATH_INFO'].endswith('culture/' + self.id + ',' + self.id2), 'incorrect culture request with list of culture IDs') 237 | 238 | def test_single_returns_single(self): 239 | add_request_intercept(sample_culture) 240 | culture = self.fs.culture(self.id) 241 | self.assertEqual(type(culture), dict, 'single culture response is wrong type') 242 | 243 | def test_list_returns_list(self): 244 | add_request_intercept(sample_culture_list) 245 | culture_list = self.fs.culture([self.id, self.id2]) 246 | self.assertEqual(type(culture_list), list, 'multiple culture response is not a list') 247 | self.assertEqual(len(culture_list), 2, 'multiple culture response has wrong length') 248 | 249 | def test_adds_one_query_param_from_kwargs(self): 250 | request_environ = add_request_intercept(sample_culture) 251 | self.fs.culture(test1='test1') 252 | self.assertIn('test1=test1', request_environ['QUERY_STRING'], 'single query parameter not included') 253 | 254 | def test_adds_multiple_query_params_from_kwargs(self): 255 | request_environ = add_request_intercept(sample_culture) 256 | self.fs.culture(test1='test1', test2='test2') 257 | self.assertIn('test1=test1', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 258 | self.assertIn('test2=test2', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 259 | 260 | def test_adds_one_query_param_from_dict(self): 261 | request_environ = add_request_intercept(sample_culture) 262 | self.fs.culture(options={'test1': 'test1'}) 263 | self.assertIn('test1=test1', request_environ['QUERY_STRING'], 'single query parameter not included') 264 | 265 | def test_adds_multiple_query_params_from_dict(self): 266 | request_environ = add_request_intercept(sample_culture) 267 | self.fs.culture(options={'test1': 'test1', 'test2': 'test2'}) 268 | self.assertIn('test1=test1', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 269 | self.assertIn('test2=test2', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 270 | 271 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 272 | request_environ = add_request_intercept(sample_culture) 273 | self.fs.culture(test1='test1', test2='test2', options={'test3': 'test3', 'test4': 'test4'}) 274 | self.assertIn('test1=test1', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 275 | self.assertIn('test2=test2', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 276 | self.assertIn('test3=test3', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 277 | self.assertIn('test4=test4', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 278 | 279 | 280 | if __name__ == '__main__': 281 | unittest.main() 282 | -------------------------------------------------------------------------------- /familysearch/tests/test_familytree.py: -------------------------------------------------------------------------------- 1 | import familysearch 2 | import unittest 3 | import wsgi_intercept.httplib_intercept 4 | try: 5 | import json 6 | except ImportError: 7 | import simplejson as json 8 | from common import * 9 | 10 | sample_person1 = load_sample('person1.json') 11 | sample_person2 = load_sample('person2.json') 12 | sample_person_list = load_sample('person_list.json') 13 | sample_persona = load_sample('persona.json') 14 | sample_persona_list = load_sample('persona_list.json') 15 | sample_version = load_sample('version.json') 16 | sample_version_list = load_sample('version_list.json') 17 | sample_pedigree = load_sample('pedigree.json') 18 | sample_pedigree_list = load_sample('pedigree_list.json') 19 | sample_search = load_sample('search.json') 20 | sample_match = load_sample('match.json') 21 | 22 | 23 | class TestFamilyTree(unittest.TestCase): 24 | 25 | def setUp(self): 26 | self.longMessage = True 27 | self.agent = 'TEST_USER_AGENT' 28 | self.key = 'FAKE_DEV_KEY' 29 | self.session = 'FAKE_SESSION_ID' 30 | self.id = 'FAKE_PERSON_ID' 31 | self.id2 = 'FAKE_PERSON_ID_2' 32 | wsgi_intercept.httplib_intercept.install() 33 | self.fs = familysearch.FamilySearch(self.agent, self.key, session=self.session) 34 | 35 | def tearDown(self): 36 | clear_request_intercpets() 37 | wsgi_intercept.httplib_intercept.uninstall() 38 | 39 | 40 | class TestFamilyTreePerson(TestFamilyTree): 41 | 42 | def test_accepts_no_arguments(self): 43 | request_environ = add_request_intercept(sample_person1) 44 | self.fs.person() 45 | self.assertTrue(request_environ['PATH_INFO'].endswith('person'), 'person request failed without a person ID') 46 | 47 | def test_accepts_me_person(self): 48 | request_environ = add_request_intercept(sample_person1) 49 | self.fs.person('me') 50 | self.assertTrue(request_environ['PATH_INFO'].endswith('person'), 'person request failed with "me"') 51 | 52 | def test_accepts_single_person(self): 53 | request_environ = add_request_intercept(sample_person1) 54 | self.fs.person(self.id) 55 | self.assertTrue(request_environ['PATH_INFO'].endswith('person/' + self.id), 'incorrect person request with single person ID') 56 | 57 | def test_accepts_list_of_persons(self): 58 | request_environ = add_request_intercept(sample_person_list) 59 | self.fs.person([self.id, self.id2]) 60 | self.assertTrue(request_environ['PATH_INFO'].endswith('person/' + self.id + ',' + self.id2), 'incorrect person request with list of person IDs') 61 | 62 | def test_single_returns_single(self): 63 | add_request_intercept(sample_person1) 64 | person = self.fs.person(self.id) 65 | self.assertEqual(type(person), dict, 'single person response is wrong type') 66 | 67 | def test_list_returns_list(self): 68 | add_request_intercept(sample_person_list) 69 | person_list = self.fs.person([self.id, self.id2]) 70 | self.assertEqual(type(person_list), list, 'multiple person response is not a list') 71 | self.assertEqual(len(person_list), 2, 'multiple person response has wrong length') 72 | 73 | def test_adds_one_query_param_from_kwargs(self): 74 | request_environ = add_request_intercept(sample_person1) 75 | self.fs.person(self.id, names='all') 76 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'single query parameter not included') 77 | 78 | def test_adds_multiple_query_params_from_kwargs(self): 79 | request_environ = add_request_intercept(sample_person1) 80 | self.fs.person(self.id, names='all', genders='all') 81 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 82 | self.assertIn('=all&', request_environ['QUERY_STRING'], 'multiple query parameters not separated properly') 83 | self.assertIn('genders=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 84 | 85 | def test_adds_one_query_param_from_dict(self): 86 | request_environ = add_request_intercept(sample_person1) 87 | self.fs.person(self.id, {'names': 'all'}) 88 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'single query parameter not included') 89 | 90 | def test_adds_multiple_query_params_from_dict(self): 91 | request_environ = add_request_intercept(sample_person1) 92 | self.fs.person(self.id, {'names': 'all', 'genders': 'all'}) 93 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 94 | self.assertIn('=all&', request_environ['QUERY_STRING'], 'multiple query parameters not separated properly') 95 | self.assertIn('genders=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 96 | 97 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 98 | request_environ = add_request_intercept(sample_person1) 99 | self.fs.person(self.id, names='all', genders='all', options={'children': 'all', 'parents': 'all'}) 100 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 101 | self.assertIn('genders=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 102 | self.assertIn('children=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 103 | self.assertIn('parents=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 104 | 105 | 106 | class TestFamilyTreePersona(TestFamilyTree): 107 | 108 | def test_does_not_accept_no_arguments(self): 109 | add_request_intercept(sample_persona) 110 | self.assertRaises(TypeError, self.fs.persona) 111 | 112 | def test_does_not_accept_me_persona(self): 113 | request_environ = add_request_intercept(sample_persona) 114 | self.fs.persona('me') 115 | self.assertFalse(request_environ['PATH_INFO'].endswith('persona'), 'persona request should not handle "me" specially') 116 | 117 | def test_accepts_single_persona(self): 118 | request_environ = add_request_intercept(sample_persona) 119 | self.fs.persona(self.id) 120 | self.assertTrue(request_environ['PATH_INFO'].endswith('persona/' + self.id), 'incorrect persona request with single persona ID') 121 | 122 | def test_accepts_list_of_personas(self): 123 | request_environ = add_request_intercept(sample_persona_list) 124 | self.fs.persona([self.id, self.id2]) 125 | self.assertTrue(request_environ['PATH_INFO'].endswith('persona/' + self.id + ',' + self.id2), 'incorrect persona request with list of persona IDs') 126 | 127 | def test_single_returns_single(self): 128 | add_request_intercept(sample_persona) 129 | persona = self.fs.persona(self.id) 130 | self.assertEqual(type(persona), dict, 'single persona response is wrong type') 131 | 132 | def test_list_returns_list(self): 133 | add_request_intercept(sample_persona_list) 134 | persona_list = self.fs.persona([self.id, self.id2]) 135 | self.assertEqual(type(persona_list), list, 'multiple persona response is not a list') 136 | self.assertEqual(len(persona_list), 2, 'multiple persona response has wrong length') 137 | 138 | def test_adds_one_query_param_from_kwargs(self): 139 | request_environ = add_request_intercept(sample_persona) 140 | self.fs.persona(self.id, names='all') 141 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'single query parameter not included') 142 | 143 | def test_adds_multiple_query_params_from_kwargs(self): 144 | request_environ = add_request_intercept(sample_persona) 145 | self.fs.persona(self.id, names='all', genders='all') 146 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 147 | self.assertIn('=all&', request_environ['QUERY_STRING'], 'multiple query parameters not separated properly') 148 | self.assertIn('genders=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 149 | 150 | def test_adds_one_query_param_from_dict(self): 151 | request_environ = add_request_intercept(sample_persona) 152 | self.fs.persona(self.id, {'names': 'all'}) 153 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'single query parameter not included') 154 | 155 | def test_adds_multiple_query_params_from_dict(self): 156 | request_environ = add_request_intercept(sample_persona) 157 | self.fs.persona(self.id, {'names': 'all', 'genders': 'all'}) 158 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 159 | self.assertIn('=all&', request_environ['QUERY_STRING'], 'multiple query parameters not separated properly') 160 | self.assertIn('genders=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 161 | 162 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 163 | request_environ = add_request_intercept(sample_persona) 164 | self.fs.persona(self.id, names='all', genders='all', options={'children': 'all', 'parents': 'all'}) 165 | self.assertIn('names=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 166 | self.assertIn('genders=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 167 | self.assertIn('children=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 168 | self.assertIn('parents=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 169 | 170 | 171 | class TestFamilyTreeVersion(TestFamilyTree): 172 | 173 | def test_does_not_accept_no_arguments(self): 174 | add_request_intercept(sample_version) 175 | self.assertRaises(TypeError, self.fs.version) 176 | 177 | def test_does_not_accept_me_version(self): 178 | request_environ = add_request_intercept(sample_version) 179 | self.fs.version('me') 180 | self.assertFalse(request_environ['PATH_INFO'].endswith('version'), 'person version request should not handle "me" specially') 181 | 182 | def test_accepts_single_person(self): 183 | request_environ = add_request_intercept(sample_version) 184 | self.fs.version(self.id) 185 | self.assertTrue(request_environ['PATH_INFO'].endswith('version/' + self.id), 'incorrect person version request with single person ID') 186 | 187 | def test_accepts_list_of_persons(self): 188 | request_environ = add_request_intercept(sample_version_list) 189 | self.fs.version([self.id, self.id2]) 190 | self.assertTrue(request_environ['PATH_INFO'].endswith('version/' + self.id + ',' + self.id2), 'incorrect person version request with list of person IDs') 191 | 192 | def test_single_returns_single(self): 193 | add_request_intercept(sample_version) 194 | version = self.fs.version(self.id) 195 | self.assertEqual(type(version), dict, 'single person version response is wrong type') 196 | 197 | def test_list_returns_list(self): 198 | add_request_intercept(sample_version_list) 199 | version_list = self.fs.version([self.id, self.id2]) 200 | self.assertEqual(type(version_list), list, 'multiple person version response is not a list') 201 | self.assertEqual(len(version_list), 2, 'multiple person version response has wrong length') 202 | 203 | def test_does_not_add_accept_kwargs(self): 204 | add_request_intercept(sample_version) 205 | self.assertRaises(TypeError, self.fs.version, self.id, names='all') 206 | 207 | def test_does_not_accept_options_dict(self): 208 | add_request_intercept(sample_version) 209 | self.assertRaises(TypeError, self.fs.version, self.id, {'names': 'all'}) 210 | 211 | 212 | class TestFamilyTreePedigree(TestFamilyTree): 213 | 214 | def test_accepts_no_arguments(self): 215 | request_environ = add_request_intercept(sample_pedigree) 216 | self.fs.pedigree() 217 | self.assertTrue(request_environ['PATH_INFO'].endswith('pedigree'), 'pedigree request failed without a person ID') 218 | 219 | def test_accepts_me_pedigree(self): 220 | request_environ = add_request_intercept(sample_pedigree) 221 | self.fs.pedigree('me') 222 | self.assertTrue(request_environ['PATH_INFO'].endswith('pedigree'), 'pedigree request failed with "me"') 223 | 224 | def test_accepts_single_pedigree(self): 225 | request_environ = add_request_intercept(sample_pedigree) 226 | self.fs.pedigree(self.id) 227 | self.assertTrue(request_environ['PATH_INFO'].endswith('pedigree/' + self.id), 'incorrect pedigree request with single person ID') 228 | 229 | def test_accepts_list_of_pedigrees(self): 230 | request_environ = add_request_intercept(sample_pedigree_list) 231 | self.fs.pedigree([self.id, self.id2]) 232 | self.assertTrue(request_environ['PATH_INFO'].endswith('pedigree/' + self.id + ',' + self.id2), 'incorrect pedigree request with list of person IDs') 233 | 234 | def test_single_returns_single(self): 235 | add_request_intercept(sample_pedigree) 236 | pedigree = self.fs.pedigree(self.id) 237 | self.assertEqual(type(pedigree), dict, 'single pedigree response is wrong type') 238 | 239 | def test_list_returns_list(self): 240 | add_request_intercept(sample_pedigree_list) 241 | pedigree_list = self.fs.pedigree([self.id, self.id2]) 242 | self.assertEqual(type(pedigree_list), list, 'multiple pedigree response is not a list') 243 | self.assertEqual(len(pedigree_list), 2, 'multiple pedigree response has wrong length') 244 | 245 | def test_adds_one_numeric_query_param_from_kwargs(self): 246 | request_environ = add_request_intercept(sample_pedigree) 247 | self.fs.pedigree(self.id, ancestors=4) 248 | self.assertIn('ancestors=4', request_environ['QUERY_STRING'], 'single query parameter not included') 249 | 250 | def test_adds_one_string_query_param_from_kwargs(self): 251 | request_environ = add_request_intercept(sample_pedigree) 252 | self.fs.pedigree(self.id, properties='all') 253 | self.assertIn('properties=all', request_environ['QUERY_STRING'], 'single query parameter not included') 254 | 255 | def test_adds_multiple_query_params_from_kwargs(self): 256 | request_environ = add_request_intercept(sample_pedigree) 257 | self.fs.pedigree(self.id, ancestors=4, properties='all') 258 | self.assertIn('ancestors=4', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 259 | self.assertIn('properties=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 260 | 261 | def test_adds_one_numeric_query_param_from_dict(self): 262 | request_environ = add_request_intercept(sample_pedigree) 263 | self.fs.pedigree(self.id, {'ancestors': 4}) 264 | self.assertIn('ancestors=4', request_environ['QUERY_STRING'], 'single query parameter not included') 265 | 266 | def test_adds_one_string_query_param_from_dict(self): 267 | request_environ = add_request_intercept(sample_pedigree) 268 | self.fs.pedigree(self.id, {'properties': 'all'}) 269 | self.assertIn('properties=all', request_environ['QUERY_STRING'], 'single query parameter not included') 270 | 271 | def test_adds_multiple_query_params_from_dict(self): 272 | request_environ = add_request_intercept(sample_pedigree) 273 | self.fs.pedigree(self.id, {'ancestors': 4, 'properties': 'all'}) 274 | self.assertIn('ancestors=4', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 275 | self.assertIn('properties=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 276 | 277 | def test_adds_query_params_from_kwargs_and_dict(self): 278 | request_environ = add_request_intercept(sample_pedigree) 279 | self.fs.pedigree(self.id, ancestors=4, options={'properties': 'all'}) 280 | self.assertIn('ancestors=4', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 281 | self.assertIn('properties=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 282 | 283 | def test_adds_query_params_from_dict_and_kwargs(self): 284 | request_environ = add_request_intercept(sample_pedigree) 285 | self.fs.pedigree(self.id, {'ancestors': 4}, properties='all') 286 | self.assertIn('ancestors=4', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 287 | self.assertIn('properties=all', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 288 | 289 | 290 | class TestFamilyTreeSearch(TestFamilyTree): 291 | 292 | def test_adds_one_query_param_from_kwargs(self): 293 | request_environ = add_request_intercept(sample_search) 294 | self.fs.search(givenName='John') 295 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'single query parameter not included') 296 | 297 | def test_adds_multiple_query_params_from_kwargs(self): 298 | request_environ = add_request_intercept(sample_search) 299 | self.fs.search(givenName='John', familyName='Smith') 300 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 301 | self.assertIn('familyName=Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 302 | 303 | def test_adds_one_query_param_from_dict(self): 304 | request_environ = add_request_intercept(sample_search) 305 | self.fs.search({'givenName': 'John'}) 306 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'single query parameter not included') 307 | 308 | def test_adds_multiple_query_params_from_dict(self): 309 | request_environ = add_request_intercept(sample_search) 310 | self.fs.search({'givenName': 'John', 'familyName': 'Smith'}) 311 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 312 | self.assertIn('familyName=Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 313 | 314 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 315 | request_environ = add_request_intercept(sample_search) 316 | self.fs.search(givenName='John', familyName='Smith', options={'father.birthPlace': 'London', 'mother.birthPlace': 'Paris'}) 317 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 318 | self.assertIn('familyName=Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 319 | self.assertIn('father.birthPlace=London', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 320 | self.assertIn('mother.birthPlace=Paris', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 321 | 322 | 323 | class TestFamilyTreeMatch(TestFamilyTree): 324 | 325 | def test_accepts_single_match(self): 326 | request_environ = add_request_intercept(sample_match) 327 | self.fs.match(self.id) 328 | self.assertTrue(request_environ['PATH_INFO'].endswith('match/' + self.id), 'incorrect match request with single person ID') 329 | 330 | def test_accepts_list_of_matches(self): 331 | request_environ = add_request_intercept(sample_match) 332 | self.fs.match([self.id, self.id2]) 333 | self.assertTrue(request_environ['PATH_INFO'].endswith('match/' + self.id + ',' + self.id2), 'incorrect match request with list of person IDs') 334 | 335 | def test_adds_one_query_param_from_kwargs(self): 336 | request_environ = add_request_intercept(sample_match) 337 | self.fs.match(givenName='John') 338 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'single query parameter not included') 339 | 340 | def test_adds_multiple_query_params_from_kwargs(self): 341 | request_environ = add_request_intercept(sample_match) 342 | self.fs.match(givenName='John', familyName='Smith') 343 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 344 | self.assertIn('familyName=Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 345 | 346 | def test_adds_one_query_param_from_dict(self): 347 | request_environ = add_request_intercept(sample_match) 348 | self.fs.match(options={'givenName': 'John'}) 349 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'single query parameter not included') 350 | 351 | def test_adds_multiple_query_params_from_dict(self): 352 | request_environ = add_request_intercept(sample_match) 353 | self.fs.match(options={'givenName': 'John', 'familyName': 'Smith'}) 354 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 355 | self.assertIn('familyName=Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 356 | 357 | def test_adds_multiple_query_params_from_kwargs_and_dict(self): 358 | request_environ = add_request_intercept(sample_match) 359 | self.fs.match(givenName='John', familyName='Smith', options={'father.birthPlace': 'London', 'mother.birthPlace': 'Paris'}) 360 | self.assertIn('givenName=John', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 361 | self.assertIn('familyName=Smith', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 362 | self.assertIn('father.birthPlace=London', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 363 | self.assertIn('mother.birthPlace=Paris', request_environ['QUERY_STRING'], 'one of multiple query parameters not included') 364 | 365 | 366 | if __name__ == '__main__': 367 | unittest.main() 368 | -------------------------------------------------------------------------------- /familysearch/tests/test_identity.py: -------------------------------------------------------------------------------- 1 | import familysearch 2 | import unittest 3 | import urllib2 4 | import wsgi_intercept.httplib_intercept 5 | try: 6 | import json 7 | except ImportError: 8 | import simplejson as json 9 | from common import * 10 | 11 | sample_login = load_sample('login.json') 12 | sample_identity_properties = load_sample('identity_properties.json') 13 | sample_request_token = load_sample('request_token.txt') 14 | sample_access_token = load_sample('access_token.txt') 15 | 16 | 17 | class TestIdentity(unittest.TestCase): 18 | 19 | def setUp(self): 20 | self.longMessage = True 21 | self.agent = 'TEST_USER_AGENT' 22 | self.key = 'FAKE_DEV_KEY' 23 | self.session = 'FAKE_SESSION_ID' 24 | self.username = 'FAKE_USERNAME' 25 | self.password = 'FAKE_PASSWORD' 26 | self.callback = 'FAKE_CALLBACK' 27 | self.temp_token = 'FAKE_TEMP_TOKEN' 28 | self.secret = 'FAKE_SECRET' 29 | self.verifier = 'FAKE_VERIFIER' 30 | self.real_token = 'FAKE_TOKEN' 31 | wsgi_intercept.httplib_intercept.install() 32 | self.fs = familysearch.FamilySearch(self.agent, self.key) 33 | 34 | def tearDown(self): 35 | clear_request_intercpets() 36 | wsgi_intercept.httplib_intercept.uninstall() 37 | 38 | 39 | class TestIdentityProperties(TestIdentity): 40 | 41 | def test_identity_properties(self): 42 | request_environ = add_request_intercept(sample_identity_properties) 43 | self.fs.identity_properties 44 | self.assertTrue(request_environ['PATH_INFO'].endswith('/properties'), 'properties request failed') 45 | 46 | def test_identity_properties_cached(self): 47 | add_request_intercept(sample_identity_properties) 48 | self.fs.identity_properties 49 | request_environ = add_request_intercept(sample_identity_properties) 50 | self.fs.identity_properties 51 | self.assertFalse(request_environ, 'properties request not cached') 52 | 53 | 54 | class TestIdentityLogin(TestIdentity): 55 | 56 | def test_fails_without_username(self): 57 | add_request_intercept(sample_login) 58 | self.assertRaises(TypeError, self.fs.login, password=self.password) 59 | 60 | def test_fails_without_password(self): 61 | add_request_intercept(sample_login) 62 | self.assertRaises(TypeError, self.fs.login, username=self.username) 63 | 64 | def test_requires_username_and_password(self): 65 | add_request_intercept(sample_login) 66 | self.fs.login(self.username, self.password) 67 | 68 | def test_login_request(self): 69 | request_environ = add_request_intercept(sample_login) 70 | self.fs.login(self.username, self.password) 71 | self.assertTrue(request_environ['PATH_INFO'].endswith('/login'), 'login request failed') 72 | post_data = request_environ['wsgi.input'].read() 73 | self.assertIn('username=' + self.username, post_data, 'login request failed to pass username') 74 | self.assertIn('password=' + self.password, post_data, 'login request failed to pass password') 75 | self.assertIn('key=' + self.key, post_data, 'login request failed to pass developer key') 76 | 77 | def test_successful_login(self): 78 | add_request_intercept(sample_login) 79 | self.assertFalse(self.fs.logged_in, 'should not be logged in initially') 80 | session_id = self.fs.login(self.username, self.password) 81 | self.assertTrue(self.fs.logged_in, 'should be logged in after login request') 82 | self.assertEqual(self.fs.session_id, self.session, 'login request failed to set correct session ID') 83 | self.assertEqual(session_id, self.session, 'login request failed to return correct session ID') 84 | 85 | def test_failed_login(self): 86 | add_request_intercept('', status='401 Unauthorized') 87 | self.fs.logged_in = True 88 | self.assertRaises(urllib2.HTTPError, self.fs.login, self.username, self.password) 89 | self.assertFalse(self.fs.logged_in, 'should not be logged in after receiving error 401') 90 | 91 | 92 | class TestIdentityInitialize(TestIdentity): 93 | 94 | def test_takes_no_arguments(self): 95 | add_request_intercept(sample_login) 96 | self.assertRaises(TypeError, self.fs.initialize, self.username) 97 | self.assertRaises(TypeError, self.fs.initialize, self.username, self.password) 98 | self.fs.initialize() 99 | 100 | def test_initialize_request(self): 101 | request_environ = add_request_intercept(sample_login) 102 | self.fs.initialize() 103 | self.assertTrue(request_environ['PATH_INFO'].endswith('/initialize'), 'initialize request failed') 104 | post_data = request_environ['wsgi.input'].read() 105 | self.assertIn('key=' + self.key, post_data, 'initialize request failed to pass developer key') 106 | 107 | def test_successful_initialize(self): 108 | add_request_intercept(sample_login) 109 | self.fs.logged_in = True 110 | session_id = self.fs.initialize() 111 | self.assertFalse(self.fs.logged_in, 'should not be logged in after initialize request') 112 | self.assertEqual(self.fs.session_id, self.session, 'initialize request failed to set correct session ID') 113 | self.assertEqual(session_id, self.session, 'initialize request failed to return correct session ID') 114 | 115 | 116 | class TestIdentityAuthenticate(TestIdentity): 117 | 118 | def test_fails_without_username(self): 119 | add_request_intercept(sample_login) 120 | self.assertRaises(TypeError, self.fs.authenticate, password=self.password) 121 | 122 | def test_fails_without_password(self): 123 | add_request_intercept(sample_login) 124 | self.assertRaises(TypeError, self.fs.authenticate, username=self.username) 125 | 126 | def test_requires_username_and_password(self): 127 | add_request_intercept(sample_login) 128 | self.fs.authenticate(self.username, self.password) 129 | 130 | def test_authenticate_request(self): 131 | request_environ = add_request_intercept(sample_login) 132 | self.fs.authenticate(self.username, self.password) 133 | self.assertTrue(request_environ['PATH_INFO'].endswith('/authenticate'), 'authenticate request failed') 134 | post_data = request_environ['wsgi.input'].read() 135 | self.assertIn('username=' + self.username, post_data, 'authenticate request failed to pass username') 136 | self.assertIn('password=' + self.password, post_data, 'authenticate request failed to pass password') 137 | 138 | def test_successful_authenticate(self): 139 | add_request_intercept(sample_login) 140 | self.fs.session_id = 'TEMP_SESSION_ID' 141 | self.assertFalse(self.fs.logged_in, 'should not be logged in initially') 142 | session_id = self.fs.authenticate(self.username, self.password) 143 | self.assertTrue(self.fs.logged_in, 'should be logged in after authenticate request') 144 | self.assertEqual(self.fs.session_id, self.session, 'authenticate request failed to set correct session ID') 145 | self.assertEqual(session_id, self.session, 'authenticate request failed to return correct session ID') 146 | 147 | def test_failed_authenticate(self): 148 | add_request_intercept('', status='401 Unauthorized') 149 | self.fs.logged_in = True 150 | self.assertRaises(urllib2.HTTPError, self.fs.authenticate, self.username, self.password) 151 | self.assertFalse(self.fs.logged_in, 'should not be logged in after receiving error 401') 152 | 153 | 154 | class TestIdentityLogout(TestIdentity): 155 | 156 | def test_logout_request(self): 157 | request_environ = add_request_intercept(sample_login) 158 | self.fs.logout() 159 | self.assertTrue(request_environ['PATH_INFO'].endswith('/logout'), 'logout request failed') 160 | 161 | def test_successful_logout(self): 162 | add_request_intercept(sample_login) 163 | self.fs.logged_in = True 164 | self.fs.session_id = self.session 165 | self.fs.logout() 166 | self.assertFalse(self.fs.logged_in, 'should not be logged in after logout request') 167 | self.assertNotEqual(self.fs.session_id, self.session, 'logout request failed to unset session ID') 168 | 169 | 170 | class TestIdentitySession(TestIdentity): 171 | 172 | def test_session_request(self): 173 | request_environ = add_request_intercept(sample_login) 174 | self.fs.session() 175 | self.assertTrue(request_environ['PATH_INFO'].endswith('/session'), 'session request failed') 176 | 177 | def test_successful_session(self): 178 | add_request_intercept(sample_login) 179 | session_id = self.fs.session() 180 | self.assertTrue(self.fs.logged_in, 'should be logged in after session request') 181 | self.assertEqual(self.fs.session_id, self.session, 'session request failed to set correct session ID') 182 | self.assertEqual(session_id, self.session, 'session request failed to return correct session ID') 183 | 184 | def test_failed_session(self): 185 | add_request_intercept('', status='401 Unauthorized') 186 | self.fs.logged_in = True 187 | self.assertRaises(urllib2.HTTPError, self.fs.session) 188 | self.assertFalse(self.fs.logged_in, 'should not be logged in after receiving error 401') 189 | 190 | 191 | class TestIdentityRequestToken(TestIdentity): 192 | 193 | def test_request_token_without_callback_request(self): 194 | add_request_intercept(sample_identity_properties) 195 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 196 | self.fs.request_token() 197 | self.assertTrue(request_environ['PATH_INFO'].endswith('/request_token'), 'request_token request failed') 198 | post_data = request_environ['wsgi.input'].read() 199 | self.assertIn('oauth_callback=oob', post_data, 'request_token request failed to pass callback') 200 | self.assertIn('oauth_consumer_key=' + self.key, post_data, 'request_token request failed to pass developer key') 201 | self.assertIn('oauth_nonce=', post_data, 'request_token request failed to pass nonce') 202 | self.assertIn('oauth_signature=%26', post_data, 'request_token request failed to pass signature') 203 | self.assertIn('oauth_signature_method=PLAINTEXT', post_data, 'request_token request failed to pass signature method') 204 | self.assertIn('oauth_timestamp=', post_data, 'request_token request failed to pass timestamp') 205 | 206 | def test_request_token_with_callback_request(self): 207 | add_request_intercept(sample_identity_properties) 208 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 209 | self.fs.request_token(callback_url=self.callback) 210 | self.assertTrue(request_environ['PATH_INFO'].endswith('/request_token'), 'request_token request failed') 211 | post_data = request_environ['wsgi.input'].read() 212 | self.assertIn('oauth_callback=' + self.callback, post_data, 'request_token request failed to pass correct callback') 213 | self.assertIn('oauth_consumer_key=' + self.key, post_data, 'request_token request failed to pass developer key') 214 | self.assertIn('oauth_nonce=', post_data, 'request_token request failed to pass nonce') 215 | self.assertIn('oauth_signature=%26', post_data, 'request_token request failed to pass signature') 216 | self.assertIn('oauth_signature_method=PLAINTEXT', post_data, 'request_token request failed to pass signature method') 217 | self.assertIn('oauth_timestamp=', post_data, 'request_token request failed to pass timestamp') 218 | 219 | def test_request_token_response(self): 220 | add_request_intercept(sample_identity_properties) 221 | add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 222 | response = self.fs.request_token() 223 | self.assertIn('oauth_token', response, 'request_token failed to return token') 224 | self.assertEqual(response['oauth_token'], self.temp_token, 'request_token failed to return correct token') 225 | self.assertIn('oauth_token_secret', response, 'request_token failed to return token secret') 226 | self.assertEqual(response['oauth_token_secret'], self.secret, 'request_token failed to return correct token secret') 227 | 228 | def test_request_token_state(self): 229 | add_request_intercept(sample_identity_properties) 230 | add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 231 | self.fs.request_token() 232 | self.assertFalse(self.fs.logged_in, 'should not be logged in after request_token request') 233 | self.assertEqual(self.fs.session_id, self.temp_token, 'request_token request failed to set correct session ID') 234 | self.assertIn(self.temp_token, self.fs.oauth_secrets, 'request_token request failed to set correct OAuth temporary credentials identifier') 235 | self.assertEqual(self.fs.oauth_secrets[self.temp_token], self.secret, 'request_token request failed to set correct OAuth temporary credentials shared-secret') 236 | 237 | 238 | class TestIdentityAuthorize(TestIdentity): 239 | 240 | def test_authorize_without_parameters(self): 241 | add_request_intercept(sample_identity_properties) 242 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 243 | url = self.fs.authorize() 244 | self.assertTrue(request_environ, 'request_token endpoint not called') 245 | self.assertEqual(url, 'http://www.dev.usys.org:2/identity/v2/authorize?sessionId=FAKE_TEMP_TOKEN', 'authorize URL incorrect') 246 | 247 | def test_authorize_with_saved_request_token_parameter(self): 248 | add_request_intercept(sample_identity_properties) 249 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 250 | self.fs.session_id = self.temp_token 251 | self.fs.oauth_secrets = {self.temp_token: self.secret} 252 | url = self.fs.authorize() 253 | self.assertFalse(request_environ, 'request_token endpoint unexpectedly called') 254 | self.assertEqual(url, 'http://www.dev.usys.org:2/identity/v2/authorize?sessionId=FAKE_TEMP_TOKEN', 'authorize URL incorrect') 255 | 256 | def test_authorize_with_request_token_parameter(self): 257 | add_request_intercept(sample_identity_properties) 258 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 259 | url = self.fs.authorize(self.temp_token) 260 | self.assertFalse(request_environ, 'request_token endpoint unexpectedly called') 261 | self.assertEqual(url, 'http://www.dev.usys.org:2/identity/v2/authorize?sessionId=FAKE_TEMP_TOKEN', 'authorize URL incorrect') 262 | 263 | def test_authorize_with_more_parameters_from_kwargs(self): 264 | add_request_intercept(sample_identity_properties) 265 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 266 | url = self.fs.authorize(self.temp_token, template='mobile') 267 | self.assertFalse(request_environ, 'request_token endpoint unexpectedly called') 268 | self.assertTrue(url.startswith('http://www.dev.usys.org:2/identity/v2/authorize?'), 'authorize URL incorrect') 269 | self.assertIn('sessionId=FAKE_TEMP_TOKEN', url, 'session ID not included') 270 | self.assertIn('template=mobile', url, 'additional parameter from kwargs not included') 271 | 272 | def test_authorize_with_more_parameters_from_dict(self): 273 | add_request_intercept(sample_identity_properties) 274 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 275 | url = self.fs.authorize(self.temp_token, {'template': 'mobile'}) 276 | self.assertFalse(request_environ, 'request_token endpoint unexpectedly called') 277 | self.assertTrue(url.startswith('http://www.dev.usys.org:2/identity/v2/authorize?'), 'authorize URL incorrect') 278 | self.assertIn('sessionId=FAKE_TEMP_TOKEN', url, 'session ID not included') 279 | self.assertIn('template=mobile', url, 'additional parameter from dict not included') 280 | 281 | def test_authorize_with_more_parameters_from_kwargs_and_dict(self): 282 | add_request_intercept(sample_identity_properties) 283 | request_environ = add_request_intercept(sample_request_token, port=1, headers={'Content-type': 'text/plain'}) 284 | url = self.fs.authorize(self.temp_token, {'template': 'mobile'}, template2='mobile') 285 | self.assertFalse(request_environ, 'request_token endpoint unexpectedly called') 286 | self.assertTrue(url.startswith('http://www.dev.usys.org:2/identity/v2/authorize?'), 'authorize URL incorrect') 287 | self.assertIn('sessionId=FAKE_TEMP_TOKEN', url, 'session ID not included') 288 | self.assertIn('template=mobile', url, 'additional parameter from dict not included') 289 | self.assertIn('template2=mobile', url, 'additional parameter from kwargs not included') 290 | 291 | 292 | class TestIdentityAccessToken(TestIdentity): 293 | 294 | def test_requires_verifier(self): 295 | add_request_intercept(sample_identity_properties) 296 | add_request_intercept(sample_access_token, port=3, headers={'Content-type': 'text/plain'}) 297 | self.assertRaises(TypeError, self.fs.access_token) 298 | self.fs.access_token(self.verifier) 299 | 300 | def test_access_token_saved_request(self): 301 | add_request_intercept(sample_identity_properties) 302 | request_environ = add_request_intercept(sample_access_token, port=3, headers={'Content-type': 'text/plain'}) 303 | # Use saved request_token and saved secret 304 | self.fs.session_id = self.temp_token 305 | self.fs.oauth_secrets = {self.temp_token: self.secret} 306 | self.fs.access_token(self.verifier) 307 | self.assertTrue(request_environ['PATH_INFO'].endswith('/access_token'), 'access_token request failed') 308 | post_data = request_environ['wsgi.input'].read() 309 | self.assertIn('oauth_consumer_key=' + self.key, post_data, 'access_token request failed to pass developer key') 310 | self.assertIn('oauth_nonce=', post_data, 'access_token request failed to pass nonce') 311 | self.assertIn('oauth_signature=%26' + self.secret, post_data, 'access_token request failed to pass signature with OAuth temporary credentials shared-secret') 312 | self.assertIn('oauth_signature_method=PLAINTEXT', post_data, 'access_token request failed to pass signature method') 313 | self.assertIn('oauth_timestamp=', post_data, 'access_token request failed to pass timestamp') 314 | self.assertIn('oauth_token=' + self.temp_token, post_data, 'access_token request failed to pass OAuth temporary credentials identifier') 315 | self.assertIn('oauth_verifier=' + self.verifier, post_data, 'access_token request failed to pass OAuth verification code') 316 | 317 | def test_access_token_with_session_id_request(self): 318 | add_request_intercept(sample_identity_properties) 319 | request_environ = add_request_intercept(sample_access_token, port=3, headers={'Content-type': 'text/plain'}) 320 | # Use saved secret but explicit request_token 321 | self.fs.oauth_secrets = {self.temp_token: self.secret} 322 | self.fs.access_token(self.verifier, request_token=self.temp_token) 323 | self.assertTrue(request_environ['PATH_INFO'].endswith('/access_token'), 'access_token request failed') 324 | post_data = request_environ['wsgi.input'].read() 325 | self.assertIn('oauth_consumer_key=' + self.key, post_data, 'access_token request failed to pass developer key') 326 | self.assertIn('oauth_nonce=', post_data, 'access_token request failed to pass nonce') 327 | self.assertIn('oauth_signature=%26' + self.secret, post_data, 'access_token request failed to pass signature with OAuth temporary credentials shared-secret') 328 | self.assertIn('oauth_signature_method=PLAINTEXT', post_data, 'access_token request failed to pass signature method') 329 | self.assertIn('oauth_timestamp=', post_data, 'access_token request failed to pass timestamp') 330 | self.assertIn('oauth_token=' + self.temp_token, post_data, 'access_token request failed to pass OAuth temporary credentials identifier') 331 | self.assertIn('oauth_verifier=' + self.verifier, post_data, 'access_token request failed to pass OAuth verification code') 332 | 333 | def test_access_token_with_session_id_and_secret_request(self): 334 | add_request_intercept(sample_identity_properties) 335 | request_environ = add_request_intercept(sample_access_token, port=3, headers={'Content-type': 'text/plain'}) 336 | # Used explicit request_token and explicit secret 337 | self.fs.access_token(self.verifier, request_token=self.temp_token, token_secret=self.secret) 338 | self.assertTrue(request_environ['PATH_INFO'].endswith('/access_token'), 'access_token request failed') 339 | post_data = request_environ['wsgi.input'].read() 340 | self.assertIn('oauth_consumer_key=' + self.key, post_data, 'access_token request failed to pass developer key') 341 | self.assertIn('oauth_nonce=', post_data, 'access_token request failed to pass nonce') 342 | self.assertIn('oauth_signature=%26' + self.secret, post_data, 'access_token request failed to pass signature with OAuth temporary credentials shared-secret') 343 | self.assertIn('oauth_signature_method=PLAINTEXT', post_data, 'access_token request failed to pass signature method') 344 | self.assertIn('oauth_timestamp=', post_data, 'access_token request failed to pass timestamp') 345 | self.assertIn('oauth_token=' + self.temp_token, post_data, 'access_token request failed to pass OAuth temporary credentials identifier') 346 | self.assertIn('oauth_verifier=' + self.verifier, post_data, 'access_token request failed to pass OAuth verification code') 347 | 348 | def test_access_token_response(self): 349 | add_request_intercept(sample_identity_properties) 350 | add_request_intercept(sample_access_token, port=3, headers={'Content-type': 'text/plain'}) 351 | response = self.fs.access_token(self.verifier) 352 | self.assertIn('oauth_token', response, 'access_token failed to return token') 353 | self.assertEqual(response['oauth_token'], self.real_token, 'access_token failed to return correct token') 354 | self.assertIn('oauth_token_secret', response, 'access_token failed to return token secret') 355 | self.assertEqual(response['oauth_token_secret'], self.secret, 'access_token failed to return correct token secret') 356 | 357 | def test_request_token_state(self): 358 | add_request_intercept(sample_identity_properties) 359 | add_request_intercept(sample_access_token, port=3, headers={'Content-type': 'text/plain'}) 360 | self.fs.session_id = self.temp_token 361 | self.fs.oauth_secrets = {self.temp_token: self.secret} 362 | self.fs.access_token(self.verifier) 363 | self.assertTrue(self.fs.logged_in, 'should be logged in after access_token request') 364 | self.assertEqual(self.fs.session_id, self.real_token, 'access_token request failed to set correct session ID') 365 | self.assertNotIn(self.temp_token, self.fs.oauth_secrets, 'access_token request failed to unset old OAuth temporary credentials identifier') 366 | 367 | def test_failed_access_token(self): 368 | add_request_intercept(sample_identity_properties) 369 | add_request_intercept('', status='401 Unauthorized', port=3) 370 | self.fs.logged_in = True 371 | self.assertRaises(urllib2.HTTPError, self.fs.access_token, self.verifier, request_token=self.temp_token, token_secret=self.secret) 372 | self.assertFalse(self.fs.logged_in, 'should not be logged in after receiving error 401') 373 | 374 | 375 | if __name__ == '__main__': 376 | unittest.main() 377 | --------------------------------------------------------------------------------