diff --git a/.gitignore b/.gitignore index 446ff23..6e0a76e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ lib include man share +__pycache__ +*.egg-info # Ignore Python byte compiled files and Windows GUI Python files *.pyc @@ -20,6 +22,8 @@ share # Ignore text editor backup files *~ +*.bak -# Ignore test/config.ini +# Ignore all config.ini file: they contain private config info test/config.ini +sample-apps/*/config.ini diff --git a/.travis.yml b/.travis.yml index ef2b550..e5035d4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,12 @@ python: - "3.2" - "3.3" - "3.4" + - "3.5" install: pip install coveralls # SDK does not have any external dependencies. - + script: coverage run --source=familysearch setup.py test after_success: coveralls diff --git a/README.rst b/README.rst index 1f6bdf9..896c89a 100644 --- a/README.rst +++ b/README.rst @@ -4,19 +4,19 @@ familysearch-python-sdk-opensource .. image:: https://readthedocs.org/projects/familysearch-python-sdk-opensource/badge/?version=latest :target: https://readthedocs.org/projects/familysearch-python-sdk-opensource/?badge=latest :alt: Documentation Status - + .. image:: https://www.codacy.com/project/badge/4875862e69c54164be173a94def06f09 - :target: https://www.codacy.com/app/elderamevans/familysearch-python-sdk-opensource + :target: https://www.codacy.com/app/djhenderson/familysearch-python-sdk-opensource :alt: Codacy Badge -.. image:: https://travis-ci.org/elderamevans/familysearch-python-sdk-opensource.svg?branch=master - :target: https://travis-ci.org/elderamevans/familysearch-python-sdk-opensource +.. image:: https://travis-ci.org/djhenderson/familysearch-python-sdk-opensource.svg?branch=master + :target: https://travis-ci.org/djhenderson/familysearch-python-sdk-opensource :alt: Build status -.. image:: https://coveralls.io/repos/elderamevans/familysearch-python-sdk-opensource/badge.svg - :target: https://coveralls.io/r/elderamevans/familysearch-python-sdk-opensource +.. image:: https://coveralls.io/repos/djhenderson/familysearch-python-sdk-opensource/badge.svg + :target: https://coveralls.io/r/djhenderson/familysearch-python-sdk-opensource :alt: Code Coverage - + .. image:: https://img.shields.io/pypi/v/familysearch-python-sdk-opensource.svg :target: https://pypi.python.org/pypi/familysearch-python-sdk-opensource :alt: PyPI Version @@ -25,13 +25,12 @@ familysearch-python-sdk-opensource :target: https://pypi.python.org/pypi/familysearch-python-sdk-opensource -This is a reboot/spinoff of https://github.com/familysearch-devnet/python-fs-stack. +This is a reboot/spinoff of https://github.com/familysearch-devnet/python-fs-stack which was last updated about June 8, 2011. + +This is a fork of https://github.com/AmEv7Fam/familysearch-python-sdk-opensource which was last updated about July 21, 2015. May or may not have all of the latest FamilySearch endpoints in place, but it is flexible enough to allow for additional endpoints It is designed for Python 3 and 2.7. Python 2.6 is the earliest supported version, but bugfixes are low-priority. 2.5 and earlier are not guaranteed to work, or supported, and are considered legacy. -To get started, visit http://elderamevans.github.io/familysearch-python-sdk-opensource. - - - +To get started, visit http://elderamevans.github.io/familysearch-python-sdk-opensource (May be 404 Not found). diff --git a/docs/Authentication.md b/docs/Authentication.md index e69de29..8ac91d0 100644 --- a/docs/Authentication.md +++ b/docs/Authentication.md @@ -0,0 +1,23 @@ +* `login(self, username, password)` +Log into FamilySearch using Basic Authentication. +This mechanism is available only to approved developer keys. + +* `oauth_desktop_login(self, ruri=None)` +Log into FamilySearch using OAuth2 Authentication. +This is primarily a convenience function for desktop apps. +Not normally intended for production apps, but should +work while waiting for approval for password login. +Default Redirect URI is "http://localhost:63342/fslogin", +but you can set your own as a paramater. + +* `oauth_code_login(self, code)` +Convenience function for Web servers to log into FamilySearch +with the token code FamilySearch hands you. + +* `unauthenticated_login(self, ip_address)` +Log into FamilySearch without authenticating. +Has very limited read-only access. +Not intended for general use. + +* `logout(self)` +Log the current session out of FamilySearch. diff --git a/docs/Authorities.md b/docs/Authorities.md index e69de29..6a5b2f2 100644 --- a/docs/Authorities.md +++ b/docs/Authorities.md @@ -0,0 +1 @@ +* [`dates(self, **kwargs)`](https://familysearch.org/developers/docs/api/dates/Date_resource) diff --git a/docs/Discovery.md b/docs/Discovery.md index e69de29..099bc04 100644 --- a/docs/Discovery.md +++ b/docs/Discovery.md @@ -0,0 +1,5 @@ +* `update_collection(self, collection)` + +* `fix_discovery(self)` +The Hypermedia items are semi-permanent. Some things change +based on who's logged in (or out). diff --git a/docs/Memories.md b/docs/Memories.md index cf8841f..77406cd 100644 --- a/docs/Memories.md +++ b/docs/Memories.md @@ -1,7 +1,7 @@ * [`memories(**kwargs)`](https://familysearch.org/developers/docs/api/memories/Memories_resource) * [`memory(mid)`](https://familysearch.org/developers/docs/api/memories/Memory_resource) * [`user_memories(**kwargs)`](https://familysearch.org/developers/docs/api/memories/User_Memories_resource) -* [`memory_personas(mid):`](https://familysearch.org/developers/docs/api/memories/Memory_Personas_resource) +* [`memory_personas(mid)`](https://familysearch.org/developers/docs/api/memories/Memory_Personas_resource) * [`memory_persona(mid, pid)`](https://familysearch.org/developers/docs/api/memories/Memory_Persona_resource) * [`memory_comments(mid)`](https://familysearch.org/developers/docs/api/memories/Memory_Comments_resource) -* [`memories_comment(mid, cmid)`](https://familysearch.org/developers/docs/api/memories/Memories_Comment_resource) \ No newline at end of file +* [`memories_comment(mid, cmid)`](https://familysearch.org/developers/docs/api/memories/Memories_Comment_resource) diff --git a/docs/Person.md b/docs/Person.md index 0e6bbdb..4d8c56d 100644 --- a/docs/Person.md +++ b/docs/Person.md @@ -16,12 +16,12 @@ * [`person_discussion_reference(pid, drid)`](https://familysearch.org/developers/docs/api/tree/Person_Discussion_References_resource) * [`person_merge(pid, dpid, **kwargs)`](https://familysearch.org/developers/docs/api/tree/Person_Merge_resource) * [`person_change_summary(pid)`](https://familysearch.org/developers/docs/api/tree/Person_Merge_resource) -* [`person_not_a_match(pid, dpid):`](https://familysearch.org/developers/docs/api/tree/Person_Not_A_Match_resource) +* [`person_not_a_match(pid, dpid)`](https://familysearch.org/developers/docs/api/tree/Person_Not_A_Match_resource) * [`person_restore(pid)`](https://familysearch.org/developers/docs/api/tree/Person_Restore_resource) * [`preferred_spouse_relationship(pid)`](https://familysearch.org/developers/docs/api/tree/Preferred_Spouse_Relationship_resource) * [`preferred_parent_relationship(pid)`](https://familysearch.org/developers/docs/api/tree/Preferred_Parent_Relationship_resource) -* [`person_memories(pid)`](https://familysearch.org/developers/docs/api/tree/Person_Memories_resource) +* [`person_memories(pid, **kwargs)`](https://familysearch.org/developers/docs/api/tree/Person_Memories_resource) * [`person_memory_references(pid)`](https://familysearch.org/developers/docs/api/tree/Person_Memory_References_resource) * [`person_memory_reference(pid, erid)`](https://familysearch.org/developers/docs/api/tree/Person_Memory_References_resource) * [`person_memories_portrait(pid, **kwargs)`](https://familysearch.org/developers/docs/api/tree/Person_Memories_Portrait_resource) -* [`person_portraits(pid)`](https://familysearch.org/developers/docs/api/tree/Person_Portraits_resource) \ No newline at end of file +* [`person_portraits(pid)`](https://familysearch.org/developers/docs/api/tree/Person_Portraits_resource) diff --git a/docs/Places.md b/docs/Places.md index e98e0ae..72ed53f 100644 --- a/docs/Places.md +++ b/docs/Places.md @@ -3,7 +3,7 @@ * [`place_group(pgid)`](https://familysearch.org/developers/docs/api/places/Place_Group_resource) * [`place(pid)`](https://familysearch.org/developers/docs/api/places/Place_resource) * [`place_description_children()`](https://familysearch.org/developers/docs/api/places/Place_Description_Children_resource) -* [`place_type(ptid):`](https://familysearch.org/developers/docs/api/places/Place_Type_resource) +* [`place_type(ptid)`](https://familysearch.org/developers/docs/api/places/Place_Type_resource) * [`place_type_group(ptgid)`](https://familysearch.org/developers/docs/api/places/Place_Type_Group_resource) * [`place_types(self)`](https://familysearch.org/developers/docs/api/places/Place_Types_resource) * [`def place_type_groups(self):`](https://familysearch.org/developers/docs/api/places/Place_Type_Groups_resource) diff --git a/docs/Sources.md b/docs/Sources.md index 6cffa88..17939cd 100644 --- a/docs/Sources.md +++ b/docs/Sources.md @@ -1,8 +1,8 @@ * [`source_descriptions()`](https://familysearch.org/developers/docs/api/sources/Source_Descriptions_resource) -* [`source_description(sdid):`](https://familysearch.org/developers/docs/api/sources/Source_Description_resource) -* [`source_folders():`](https://familysearch.org/developers/docs/api/sources/Source_Folders_resource) -* [`source_folder(udcid):`](https://familysearch.org/developers/docs/api/sources/Source_Folder_resource) +* [`source_description(sdid)`](https://familysearch.org/developers/docs/api/sources/Source_Description_resource) +* [`source_folders()`](https://familysearch.org/developers/docs/api/sources/Source_Folders_resource) +* [`source_folder(udcid)`](https://familysearch.org/developers/docs/api/sources/Source_Folder_resource) * [`source_folder_source_descriptions(udcid, **kwargs)`](https://familysearch.org/developers/docs/api/sources/Source_Folder_Source_Descriptions_resource) * [`user_source_folders()`](https://familysearch.org/developers/docs/api/sources/User_Source_Folders_resource) -* [`user_source_descriptions(**kwargs):`](https://familysearch.org/developers/docs/api/sources/User_Source_Descriptions_resource) -* [`source_references_query(self, **kwargs):`](https://familysearch.org/developers/docs/api/tree/Source_References_Query_resource) \ No newline at end of file +* [`user_source_descriptions(**kwargs)`](https://familysearch.org/developers/docs/api/sources/User_Source_Descriptions_resource) +* [`source_references_query(self, **kwargs)`](https://familysearch.org/developers/docs/api/tree/Source_References_Query_resource) diff --git a/docs/index.md b/docs/index.md index 6d5c51c..f46a5c1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,8 @@ -Modules are modeled after the layout of [the official FamilySearch API documentation](https://familysearch.org/developers/docs/api/resources). -If you're not sure which function you need, it's best to use the URL of the FamilySearch Developer documentation, and search in that section for that URL. +Modules are modeled after the layout of +[the official FamilySearch API documentation](https://familysearch.org/developers/docs/api/resources). +If you're not sure which function you need, +it's best to use the URL of the FamilySearch Developer documentation, +and search in that section for that URL. * [Main Module](Main_Module.md) * [Authentication](Authentication.md) diff --git a/familysearch/__init__.py b/familysearch/__init__.py index 9ab83d4..c2074f9 100644 --- a/familysearch/__init__.py +++ b/familysearch/__init__.py @@ -1,6 +1,9 @@ +# -*- coding: utf-8 -*- + # For PyLint users: -# I'll get back to you to the recommeneded command. -r"""This is a WIP, unofficial SDK for accessing +# I'll get back to you to the recommended command. + +"""This is a WIP, unofficial SDK for accessing the FamilySearch API. Currently designed to support Python 3.2+ and 2.6+. @@ -24,19 +27,20 @@ # Python imports - try: # Python 3 from urllib.request import Request as BaseRequest from urllib.request import build_opener from urllib.error import HTTPError from urllib.parse import(urlsplit, urlunsplit, parse_qs, urlencode) + PY3 = True except ImportError: # Python 2 from urllib import urlencode from urllib2 import Request as BaseRequest from urllib2 import build_opener, HTTPError from urlparse import(urlsplit, urlunsplit, parse_qs) + PY3 = False import json import time @@ -64,13 +68,14 @@ # Magic -__version__ = '1.2.0' +__version__ = '1.3.0alpha' class Request(BaseRequest): """Add ability for the Request object to allow it to handle additional methods. """ + def __init__(self, *args, **kwargs): self._method = kwargs.pop('method', None) BaseRequest.__init__(self, *args, **kwargs) @@ -79,6 +84,7 @@ def get_method(self): """The Request object has been enhanced to handle PUT, DELETE, OPTIONS, and HEAD request methods. """ + if self._method: return self._method else: @@ -96,22 +102,21 @@ class FamilySearch(Authentication, Authorities, ChangeHistory, Discovery, ...needs to be re-written... """ - def __init__(self, agent, key, session=None, + def __init__(self, agent, dev_key, session=None, base='https://sandbox.familysearch.org'): - """Instantiate a FamilySearch proxy object. Keyword arguments: agent -- User-agent string to use for requests - key -- FamilySearch developer key + dev_key -- FamilySearch developer key (optional if reusing an existing session ID) session (optional) -- existing session ID to reuse base (optional) -- base URL for the API; defaults to 'https://sandbox.familysearch.org' """ - self.agent = '%s FSPySDK/%s' % (agent, __version__) - self.key = key + self.user_agent = '%s FSPySDK/%s' % (agent, __version__) + self.dev_key = dev_key self.access_token = session self.base = base self.user_base = self.base + "/platform/users/" @@ -120,13 +125,15 @@ def __init__(self, agent, key, session=None, self.logged_in = bool(self.access_token) Discovery.__init__(self) - super().__init__() + if PY3: + super().__init__() + else: + super(FamilySearch, self).__init__() # Discovery needs to explicitly be first, as it is the hypermedia # engine. # There might be a better fix, but it's better than nothing. - def _request(self, url, data=None, headers=None, method=None, nojson=False): """ Make a request to the FamilySearch API. @@ -137,6 +144,7 @@ def _request(self, url, data=None, headers=None, method=None, nojson=False): Returns a file-like object representing the response. """ + if headers is None: headers = {} if data: @@ -148,26 +156,28 @@ def _request(self, url, data=None, headers=None, method=None, nojson=False): try: data = data.encode('utf-8') except AttributeError: - #Some things are not JSON, and need to be sent as bytes! + # Some things are not JSON, and need to be sent as bytes! pass request = Request(url, data, headers, method=method) if not nojson: if method is not "GET" and "Content-type" not in request.headers: request.add_header('Content-type', 'application/x-fs-v1+json') if 'Accept' not in request.headers: - request.add_header('Accept', 'application/json') + request.add_header('Accept', 'application/x-fs-v1+json;application/x-gedcomx-v1+json;application/x-gedcomx-atom+json;application/json') + if 'Accept-Charset' not in request.headers: + request.add_header('Accept-Charset', 'UTF-8') if self.logged_in and not self.cookies: # Add sessionId parameter to url if cookie is not set request.add_header('Authorization', 'Bearer ' + self.access_token) - request.add_header('User-Agent', self.agent) + request.add_header('User-Agent', self.user_agent) try: return self.opener.open(request) except HTTPError as error: - eh = dict(error.headers) + error_headers = dict(error.headers) if error.code == 401: self.logged_in = False if error.code == 429: - time.sleep(eh['Retry-after']/1000) + time.sleep(error_headers['Retry-after'] / 1000) return self._request(url, data, headers, method, nojson) raise @@ -177,8 +187,8 @@ def _add_subpath(self, url, subpath): For example, adding sub to http://example.com/path?query becomes http://example.com/path/sub?query. - """ + parts = urlsplit(url) path = parts[2] + '/' + subpath return urlunsplit((parts[0], parts[1], path, parts[3], parts[4])) @@ -187,6 +197,7 @@ def _add_query_params(self, url, params=None, **kw_params): """Add the specified query parameters to the given URL. Parameters can be passed either as a dictionary or as keyword arguments. """ + if params is None: params = {} parts = urlsplit(url) @@ -202,6 +213,7 @@ def _fs2py(self, response, nojson=False): Take JSON from FamilySearch response, and allow Python to handle it. Also, inject headers into response. """ + headers = dict(response.info()) response = response.read() response = response.decode("utf-8") @@ -218,25 +230,30 @@ def get(self, url, data=None, headers=None, nojson=False): def post(self, url, data=None, headers=None, nojson=False): """HTTP POST request""" + return self._fs2py(self._request( url, data, headers, "POST", nojson), nojson) def put(self, url, data=None, headers=None, nojson=False): """HTTP PUT request""" + return self._fs2py(self._request( url, data, headers, "PUT", nojson), nojson) def head(self, url, data=None, headers=None, nojson=False): """HTTP HEAD request""" + return self._fs2py(self._request( url, data, headers, "HEAD", nojson), nojson) def options(self, url, data=None, headers=None, nojson=False): """HTTP OPTIONS request""" + return self._fs2py(self._request( url, data, headers, "OPTIONS", nojson), nojson) def delete(self, url, data=None, headers=None, nojson=False): """HTTP DELETE request""" + return self._fs2py(self._request( url, data, headers, "DELETE", nojson), nojson) diff --git a/familysearch/authentication.py b/familysearch/authentication.py index 73c3198..dcbd35f 100644 --- a/familysearch/authentication.py +++ b/familysearch/authentication.py @@ -1,5 +1,9 @@ +# -*- coding: utf-8 -*- + """FamilySearch Authentication submodule""" + # Python imports +from __future__ import print_function try: # Python 3 @@ -18,33 +22,49 @@ # Magic + class Authentication(object): + def __init__(self): """https://familysearch.org/developers/docs/api/resources#authentication Set up the URLs for authentication. """ + self.token = self.root_collection['response']['collections'][0]['links']\ - ['http://oauth.net/core/2.0/endpoint/token']['href'] + ['http://oauth.net/core/2.0/endpoint/token']['href'] cookie_handler = HTTPCookieProcessor() self.cookies = cookie_handler.cookiejar self.opener = build_opener(cookie_handler) + self.logged_in = False + self.access_token = None def login(self, username, password): """Log into FamilySearch using Basic Authentication. This mechanism is available only to approved developer keys. """ + self.logged_in = False self.cookies.clear() url = self.token credentials = urlencode({'username': username, 'password': password, - 'client_id': self.key, + 'client_id': self.dev_key, 'grant_type': 'password' - }) + } + ) credentials = credentials.encode("utf-8") headers = {"Content-type": "application/x-www-form-urlencoded", - "Accept": "application/json"} + "Accept": "application/x-fs-v1+json;application/x-gedcomx-v1+json;application/x-gedcomx-atom+json;application/json", + "Accept-Charset": "utf-8"} + + def xprint(): + print("url:", url) + print("credentials:", credentials) + print("headers:", headers) + + xprint() response = self.post(url, credentials, headers) + print("authentication.login:response:", response) self.access_token = response['response']['access_token'] self.logged_in = True self.fix_discovery() @@ -52,22 +72,25 @@ def login(self, username, password): def oauth_desktop_login(self, ruri=None): """ Log into FamilySearch using OAuth2 Authentication. - This is primarily a convenience function for destop apps. + This is primarily a convenience function for desktop apps. Not normally intended for production apps, but should work while waiting for approval for password login. Default Redirect URI is "http://localhost:63342/fslogin", but you can set your own as a paramater. """ + if ruri is None: ruri = "http://localhost:63342/fslogin" self.logged_in = False self.cookies.clear() url = self.root_collection['response']['collections'][0]['links']\ - ['http://oauth.net/core/2.0/endpoint/authorize']['href'] - url = self._add_query_params(url, {'response_type': 'code', - 'client_id': self.key, - 'redirect_uri': ruri - }) + ['http://oauth.net/core/2.0/endpoint/authorize']['href'] + url = self._add_query_params(url, + {'response_type': 'code', + 'client_id': self.dev_key, + 'redirect_uri': ruri, + } + ) webbrowser.open(url) server.HTTPServer(('', 63342), Getter).handle_request() # Now that we have the authentication token, grab the access token. @@ -78,17 +101,26 @@ def oauth_code_login(self, code): Convenience function for Web servers to log into FamilySearch with the token code FamilySearch hands you. """ + url = self.token credentials = urlencode({'grant_type': 'authorization_code', 'code': code, - 'client_id': self.key - }) + 'client_id': self.dev_key, + } + ) credentials = credentials.encode("utf-8") headers = {"Content-type": "application/x-www-form-urlencoded", - "Accept": "application/json"} - response = self.post(url, credentials ,headers, nojson=True) - response = json.loads() - self.access_token = response['response']['access_token'] + "Accept": "application/x-fs-v1+json;application/x-gedcomx-v1+json;application/x-gedcomx-atom+json;application/json", + "Accept-Charset": "utf-8"} + response = self.post(url, credentials, headers, nojson=True) + def rprint(response): + import pprint + pp = pprint.PrettyPrinter(indent=2, width=120) + pp.pprint(response) + #rprint(response) + json_response = json.loads(response["response"]) + self.access_token = json_response['access_token'] + print("access_token:", self.access_token) self.logged_in = True self.fix_discovery() @@ -98,16 +130,18 @@ def unauthenticated_login(self, ip_address): Has very limited read-only access. Not intended for general use. """ + self.logged_in = False self.cookies.clear() url = self.token credentials = urlencode({'ip_address': ip_address, - #TODO: make IP address generiation automatic - 'client_id': self.key, + # TODO: make IP address generation automatic + 'client_id': self.dev_key, 'grant_type': 'unauthenticated_session' - }) + }) headers = {"Content-type": "application/x-www-form-urlencoded", - "Accept": "application/json"} + "Accept": "application/x-fs-v1+json;application/x-gedcomx-v1+json;application/x-gedcomx-atom+json;application/json", + "Accept-Charset": "utf-8"} credentials = credentials.encode("utf-8") response = self.post(url, credentials, headers) self.access_token = response['response']['access_token'] @@ -118,6 +152,7 @@ def logout(self): """ Log the current session out of FamilySearch. """ + self.logged_in = False url = self.token + "?access_token=" + self.access_token self.delete(url) @@ -125,12 +160,15 @@ def logout(self): self.cookies.clear() self.fix_discovery() + class Getter(server.BaseHTTPRequestHandler): """Sample login page, mostly for oauth_desktop_login.""" + def do_GET(self): """Sample page to get Oauth code, and log in with.""" + self.send_response(code=200) - self.send_header("Content-type", "text/html") + self.send_header("Content-type", "text/html;charset=utf-8") self.end_headers() path = self.path global qs @@ -141,4 +179,3 @@ def do_GET(self): sendme += "

You can safely close this page.

" sendme = sendme.encode("UTF-8") self.wfile.write(sendme) - \ No newline at end of file diff --git a/familysearch/authorities.py b/familysearch/authorities.py index 232837a..2e237ce 100644 --- a/familysearch/authorities.py +++ b/familysearch/authorities.py @@ -1,17 +1,23 @@ +# -*- coding: utf-8 -*- + """FamilySearch Authorities submodule""" + # Python imports - + # Magic + class Authorities(): """https://familysearch.org/developers/docs/api/resources#authorities""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#authorities.""" + pass def dates(self, **kwargs): """https://familysearch.org/developers/docs/api/dates/Date_resource.""" - + try: url = self.collections['FSDA']['response']['collections'][0][ 'links']['normalized-date']['template'] @@ -21,4 +27,4 @@ def dates(self, **kwargs): 'links']['normalized-date']['template'] shim = {} shim["?date,access_token"] = "" - return self._add_query_params(url.format(**shim), kwargs) \ No newline at end of file + return self._add_query_params(url.format(**shim), kwargs) diff --git a/familysearch/changeHistory.py b/familysearch/changeHistory.py index ab4152f..f0300d7 100644 --- a/familysearch/changeHistory.py +++ b/familysearch/changeHistory.py @@ -1,29 +1,39 @@ +# -*- coding: utf-8 -*- + """FamilySearch Change History submodule""" + # Python imports # Magic + class ChangeHistory: """https://familysearch.org/developers/docs/api/resources#authorities""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#change-history""" + pass def person_change_history(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Change_History_resource""" - return self.base + "/platform/tree/persons/"+ pid +"/changes" + + return self.base + "/platform/tree/persons/" + pid + "/changes" def child_change_history(self, caprid): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_Change_History_resource""" + return self.tree_base + "child-and-parents-relationships/"\ + caprid + "/changes" def couple_change_history(self, crid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Couple_Relationship_Change_History_resource""" + return self._add_query_params( self.tree_base + "couple-relationships/" + crid + "/changes", kwargs) def restore_change(self, chid): """https://familysearch.org/developers/docs/api/tree/Restore_Change_resource""" - return self.tree_base + "changes/" + chid + "/restore" \ No newline at end of file + + return self.tree_base + "changes/" + chid + "/restore" diff --git a/familysearch/discovery.py b/familysearch/discovery.py index a5a542b..80f4745 100644 --- a/familysearch/discovery.py +++ b/familysearch/discovery.py @@ -1,24 +1,30 @@ +# -*- coding: utf-8 -*- + """FamilySearch Discovery submodule""" + # Python imports # Magic + class Discovery(object): """https://familysearch.org/developers/docs/api/tree/FamilySearch_Collections_resource""" + def __init__(self): """https://familysearch.org/developers/docs/api/resources#discovery""" - # TODO: Set it up so that it doesn't need to call the sumbodules + + # TODO: Set it up so that it doesn't need to call the submodules # until absolutely necessary... self.root_collection = self.get(self.base + '/.well-known/collection') self.subcollections = self.get(self.root_collection['response'] - ['collections'][0]['links'] - ['subcollections']['href']) + ['collections'][0]['links'] + ['subcollections']['href']) self.collections = {} self.fix_discovery() def update_collection(self, collection): - response = self.get(self.collections[collection]['url'])['response'] - self.collections[collection]['response'] = response + response = self.get(self.collections[collection]['url'])['response'] + self.collections[collection]['response'] = response def fix_discovery(self): """The Hypermedia items are semi-permanent. Some things change @@ -30,11 +36,11 @@ def fix_discovery(self): if item['id'] == 'LDSO': try: self.update_collection("LDSO") - except: + except KeyError: self.lds_user = False else: self.lds_user = True try: self.user = self.get_current_user()['response']['users'][0] except: - self.user = "" \ No newline at end of file + self.user = "" diff --git a/familysearch/discussions.py b/familysearch/discussions.py index 5caaaad..2f9283f 100644 --- a/familysearch/discussions.py +++ b/familysearch/discussions.py @@ -1,14 +1,18 @@ +# -*- coding: utf-8 -*- + """FamilySearch Discussions submodule""" + # Python imports # Magic + class Discussions: """https://familysearch.org/developers/docs/api/resources#discussions""" + def __init__(self): """https://familysearch.org/developers/docs/api/resources#discussions""" self.discuss_base = self.base + "/platform/discussions/discussions/" - pass def discussions(self): """https://familysearch.org/developers/docs/api/discussions/Discussions_resource""" @@ -24,4 +28,4 @@ def discussion_comments(self, did): def discussion_comment(self, did, cmid): """https://familysearch.org/developers/docs/api/discussions/Comment_resource""" - return self.discuss_base + did + "/comments/" + cmid \ No newline at end of file + return self.discuss_base + did + "/comments/" + cmid diff --git a/familysearch/memories.py b/familysearch/memories.py index d9c2ac5..b23e74a 100644 --- a/familysearch/memories.py +++ b/familysearch/memories.py @@ -1,40 +1,53 @@ +# -*- coding: utf-8 -*- + """FamilySearch Memories submodule""" + # Python imports # Magic + class Memories: """https://familysearch.org/developers/docs/api/resources#memories""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#memories""" + self.memory_base = self.tree_base + "/platform/memories/memories/" def memories(self, **kwargs): """https://familysearch.org/developers/docs/api/memories/Memories_resource""" + return self._add_query_params(self.memory_base[::-1], kwargs) def memory(self, mid): """https://familysearch.org/developers/docs/api/memories/Memory_resource""" + return self.memory_base + mid def user_memories(self, **kwargs): """https://familysearch.org/developers/docs/api/memories/User_Memories_resource""" + return self._add_query_params( self.memory_base + "users/" + self.user['userId'] + "/memories", kwargs) def memory_personas(self, mid): """https://familysearch.org/developers/docs/api/memories/Memory_Personas_resource""" + return self.memory_base + mid + "personas" - + def memory_persona(self, mid, pid): """https://familysearch.org/developers/docs/api/memories/Memory_Persona_resource""" + return self.memory_base + mid + "personas/" + pid - + def memory_comments(self, mid): """https://familysearch.org/developers/docs/api/memories/Memory_Comments_resource""" + return self.memory_base + mid + "comments" def memories_comment(self, mid, cmid): """https://familysearch.org/developers/docs/api/memories/Memories_Comment_resource""" - return self.memory_base + mid + "comments/" + cmid \ No newline at end of file + + return self.memory_base + mid + "comments/" + cmid diff --git a/familysearch/ordinances.py b/familysearch/ordinances.py index 5f79046..4513a59 100644 --- a/familysearch/ordinances.py +++ b/familysearch/ordinances.py @@ -1,10 +1,16 @@ +# -*- coding: utf-8 -*- + """FamilySearch Ordinances submodule""" + # Python imports # Magic + class Ordinances: """https://familysearch.org/developers/docs/api/resources#ordinances""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#ordinances""" - pass # Placeholder file. \ No newline at end of file + + pass # Placeholder file. diff --git a/familysearch/parentsAndChildren.py b/familysearch/parentsAndChildren.py index d44c757..f7de681 100644 --- a/familysearch/parentsAndChildren.py +++ b/familysearch/parentsAndChildren.py @@ -1,35 +1,48 @@ +# -*- coding: utf-8 -*- + """FamilySearch Parents and Children submodule""" + # Python imports # Magic + class ParentsAndChildren: """https://familysearch.org/developers/docs/api/resources#parents-and-children""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#parents-and-children""" + self.child_base = self.tree_base + "child-and-parents-relationships/" def child_relationship(self, crid): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_resource""" + return self.child_base + crid def child_relationship_parent(self, crid, role): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_Parent_resource""" + return self.child_base + crid + "/" + role def child_relationship_conclusion(self, crid, role, cid): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_Conclusion_resource""" + return self.delete( - self.child_base + crid + "/" + role + "/conclusions/" + cid) - + self.child_base + crid + "/" + role + "/conclusions/" + cid) + def child_relationship_notes(self, crid): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_Notes_resource""" + + return self.child_base + crid + "/notes" - + def child_relationship_note(self, crid, nid): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_Note_resource""" + return self.child_base + crid + "/notes/" + nid def child_relationship_restore(self, crid): """https://familysearch.org/developers/docs/api/tree/Child-and-Parents_Relationship_Restore_resource""" - return self.child_base + crid + "/restore" \ No newline at end of file + + return self.child_base + crid + "/restore" diff --git a/familysearch/pedigree.py b/familysearch/pedigree.py index c718eee..965f292 100644 --- a/familysearch/pedigree.py +++ b/familysearch/pedigree.py @@ -1,20 +1,28 @@ +# -*- coding: utf-8 -*- + """FamilySearch Pedigree submodule""" + # Python imports # Magic + class Pedigree: """https://familysearch.org/developers/docs/api/examples#pedigree""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#pedigree""" + pass - def ancestry(self, pid,**kwargs): + def ancestry(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Ancestry_resource""" + return self._add_query_params( self.tree_base + 'ancestry?person=' + pid, kwargs) def descendancy(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Descendancy_resource""" + return self._add_query_params( self.tree_base + 'descendancy?person=' + pid, kwargs) diff --git a/familysearch/person.py b/familysearch/person.py index 45e8945..1ded840 100644 --- a/familysearch/person.py +++ b/familysearch/person.py @@ -1,128 +1,161 @@ +# -*- coding: utf-8 -*- + """FamilySearch Person submodule""" + # Python imports # Magic + class Person: """https://familysearch.org/developers/docs/api/examples#person""" + def __init__(self): """https://familysearch.org/developers/docs/api/resources#person""" + self.person_base = self.tree_base + 'persons/' def persons(self): """https://familysearch.org/developers/docs/api/tree/Persons_resource""" + return self.person_base[::-1] def person(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_resource""" + return self.person_base + pid def person_parents(self, pid): """https://familysearch.org/developers/docs/api/tree/Parents_of_a_Person_resource""" + return self.person_base + pid + "/parents" def person_spouses(self, pid): """https://familysearch.org/developers/docs/api/tree/Spouses_of_a_Person_resource""" + return self.person_base + pid + "/spouses" def person_children(self, pid): """https://familysearch.org/developers/docs/api/tree/Children_of_a_Person_resource""" - return self.get(self.person_base+pid+"/children") + + return self.get(self.person_base + pid + "/children") def spouse_relationships(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Relationships_to_Spouses_resource""" + return self._add_query_params( self.person_base + pid + '/spouse-relationships', kwargs) def child_relationships(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Relationships_to_Children_resource""" + return self._add_query_params( self.person_base + pid + '/child-relationships', kwargs) def parent_relationships(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Relationships_to_Parents_resource""" + return self._add_query_params( self.person_base + pid + '/parent_relationships', kwargs) def person_with_relationships(self, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_With_Relationships_resource""" + return self._add_query_params( self.tree_base + "persons-with-relationships", kwargs) def person_conclusion(self, pid, cid): """https://familysearch.org/developers/docs/api/tree/Person_Conclusion_resource""" + return self.person_base + pid + "conclusions" + cid - + def person_source_references(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Source_References_resource""" + return self.person_base + pid + "source-references" def person_source_reference(self, pid, srid): """https://familysearch.org/developers/docs/api/tree/Person_Source_References_resource""" + return self.person_base + pid + "source-references/" + srid - + def person_sources_query(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Sources_Query_resource""" + return self.person_base + pid + "sources" def person_note(self, pid, nid): """https://familysearch.org/developers/docs/api/tree/Person_Note_resource""" + return self.person_base + pid + "notes" + nid def person_discussion_references(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Discussion_References_resource""" + return self.person_base + pid + "/discussion_reference" def person_discussion_reference(self, pid, drid): """https://familysearch.org/developers/docs/api/tree/Person_Discussion_References_resource""" + return self.person_base + pid + "/discussion_reference/" + drid def person_merge(self, pid, dpid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Merge_resource""" + return self._add_query_params( self.person_base + pid + "/merges/" + dpid, kwargs) def person_change_summary(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Merge_resource""" + return self.person_base + pid + "/change-summary" def person_not_a_match(self, pid, dpid): """https://familysearch.org/developers/docs/api/tree/Person_Not_A_Match_resource""" + return self.person_base + pid + "/not-a-match/" + dpid def person_restore(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Restore_resource""" + return self.person_base + pid + "/restore" def preferred_spouse_relationship(self, pid, uid=None): """https://familysearch.org/developers/docs/api/tree/Preferred_Spouse_Relationship_resource""" + if uid is None: uid = self.user['userId'] return self.user_base + uid + "/preferred-spouse-relationships/" + pid def preferred_parent_relationship(self, pid, uid=None): """https://familysearch.org/developers/docs/api/tree/Preferred_Parent_Relationship_resource""" + if uid is None: uid = self.user['userId'] - return self.user_base + uid +"/preferred-parent-relationships/" + pid + return self.user_base + uid + "/preferred-parent-relationships/" + pid - def person_memories(self, pid): + def person_memories(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Memories_resource""" + return self._add_query_params( self.person_base + pid + "/memories", kwargs) def person_memory_references(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Memory_References_resource""" + return self.person_base + pid + "/memory-references" def person_memory_reference(self, pid, erid): """https://familysearch.org/developers/docs/api/tree/Person_Memory_References_resource""" + return self.person_base + pid + "/memory-references/" + erid def person_memories_portrait(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Memories_Portrait_resource""" + return self._add_query_params(self.person_base + pid + "/portrait", kwargs) def person_portraits(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Portraits_resource""" - return self.person_base + "/portrait" \ No newline at end of file + + return self.person_base + "/portrait" diff --git a/familysearch/places.py b/familysearch/places.py index 86477f7..c08e75c 100644 --- a/familysearch/places.py +++ b/familysearch/places.py @@ -1,28 +1,38 @@ +# -*- coding: utf-8 -*- + """FamilySearch Places submodule""" + # Python imports # Magic + class Places: """https://familysearch.org/developers/docs/api/resources#places""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#places""" + self.places_base = self.base + "/platform/places/" def places_search(self, **kwargs): """https://familysearch.org/developers/docs/api/places/Places_Search_resource""" + return self._add_query_params(self.places_base + "search", kwargs) def place_description(self, pdid): """https://familysearch.org/developers/docs/api/places/Place_Description_resource""" + return self.places_base + "description/" + pdid def place_group(self, pgid): """https://familysearch.org/developers/docs/api/places/Place_Group_resource""" + return self.places_base + "groups/" + pgid def place(self, pid): """https://familysearch.org/developers/docs/api/places/Place_resource""" + return self.places_base + pid def place_description_children(self): @@ -31,16 +41,20 @@ def place_description_children(self): def place_type(self, ptid): """https://familysearch.org/developers/docs/api/places/Place_Type_resource""" + return self.places_base + "types/" + ptid def place_type_group(self, ptgid): """https://familysearch.org/developers/docs/api/places/Place_Type_Group_resource""" + return self.places_base + "type-groups/" + ptgid def place_types(self): """https://familysearch.org/developers/docs/api/places/Place_Types_resource""" + return self.places_base + "types" def place_type_groups(self): """https://familysearch.org/developers/docs/api/places/Place_Type_Groups_resource""" - return self.places_base + "type-groups" \ No newline at end of file + + return self.places_base + "type-groups" diff --git a/familysearch/records.py b/familysearch/records.py index 322b5cc..d22301c 100644 --- a/familysearch/records.py +++ b/familysearch/records.py @@ -1,12 +1,18 @@ +# -*- coding: utf-8 -*- + """FamilySearch Records submodule""" + # Python imports # Magic + class Records: """https://familysearch.org/developers/docs/api/resources#records""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#records""" + pass # There might be a way to reverse-engineer something from the resource, diff --git a/familysearch/searchAndMatch.py b/familysearch/searchAndMatch.py index cacac9d..f9309cd 100644 --- a/familysearch/searchAndMatch.py +++ b/familysearch/searchAndMatch.py @@ -1,26 +1,37 @@ +# -*- coding: utf-8 -*- + """FamilySearch Search and Match submodule""" + # Python imports # Magic + class SearchAndMatch: """https://familysearch.org/developers/docs/api/resources#search-and-match""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#search-and-match""" + pass + def person_search(self, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Search_resource""" + return self._add_query_params(self.tree_base + "search", kwargs) def person_matches(self, pid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Matches_resource""" + return self._add_query_params( self.tree_base + "persons/" + pid + "/matches", kwargs) def person_not_a_match_list(self, pid): """https://familysearch.org/developers/docs/api/tree/Person_Not_A_Match_List_resource""" + return self.tree_base + "persons/" + pid + "/not-a-match" def person_matches_query(self, **kwargs): """https://familysearch.org/developers/docs/api/tree/Person_Matches_Query_resource""" - return self._add_query_params(self.tree_base + "matches", kwargs) \ No newline at end of file + + return self._add_query_params(self.tree_base + "matches", kwargs) diff --git a/familysearch/sources.py b/familysearch/sources.py index 463cfa7..601c6c6 100644 --- a/familysearch/sources.py +++ b/familysearch/sources.py @@ -1,45 +1,59 @@ +# -*- coding: utf-8 -*- + """FamilySearch Vocabularies submodule""" + # Python imports # Magic + class Sources: """https://familysearch.org/developers/docs/api/resources#sources""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#sources""" + self.source_base = self.base + "/platform/sources/" def source_descriptions(self): """https://familysearch.org/developers/docs/api/sources/Source_Descriptions_resource""" + return self.source_base + "descriptions" def source_description(self, sdid): """https://familysearch.org/developers/docs/api/sources/Source_Description_resource""" + return self.source_base + "descriptions/" + sdid def source_folders(self): """https://familysearch.org/developers/docs/api/sources/Source_Folders_resource""" + return self.source_base + "collections" def source_folder(self, udcid): """https://familysearch.org/developers/docs/api/sources/Source_Folder_resource""" + return self.source_base + "collections/" + udcid def source_folder_source_descriptions(self, udcid, **kwargs): """https://familysearch.org/developers/docs/api/sources/Source_Folder_Source_Descriptions_resource""" + return self._add_query_params( self.source_base + "collections/" + udcid + "/descriptions", kwargs) def user_source_folders(self): """https://familysearch.org/developers/docs/api/sources/User_Source_Folders_resource""" + return self.source_base + self.user['personId'] + "/collections" def user_source_descriptions(self, **kwargs): """https://familysearch.org/developers/docs/api/sources/User_Source_Descriptions_resource""" + return self._add_query_params( self.source_base + self.user['personId'] + "/collections", kwargs) def source_references_query(self, **kwargs): """https://familysearch.org/developers/docs/api/tree/Source_References_Query_resource""" + return self._add_query_params( - self.tree_base + "source_references", kwargs) \ No newline at end of file + self.tree_base + "source_references", kwargs) diff --git a/familysearch/spouses.py b/familysearch/spouses.py index d7a1ee3..73ad249 100644 --- a/familysearch/spouses.py +++ b/familysearch/spouses.py @@ -1,34 +1,46 @@ +# -*- coding: utf-8 -*- + """FamilySearch Spouses submodule""" + # Python imports # Magic + class Spouses: """https://familysearch.org/developers/docs/api/resources#spouses""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#spouses""" + self.couple_base = self.tree_base + 'couple-relationships/' def relationship(self): """https://familysearch.org/developers/docs/api/tree/Relationships_resource""" + return self.tree_base + "relationships" def couple_relationship(self, cpid, **kwargs): """https://familysearch.org/developers/docs/api/tree/Couple_Relationship_resource""" + return self._add_query_params(self.couple_base + cpid, kwargs) def couple_relationship_conclusion(self, cpid, cid): """https://familysearch.org/developers/docs/api/tree/Couple_Relationship_Conclusion_resource""" + return self.couple_base + cpid + '/conclusions/' + cid - + def couple_relationship_notes(self, crid): """https://familysearch.org/developers/docs/api/tree/Couple_Relationship_Notes_resource""" + return self.couple_base + crid + "/notes" def couple_relationship_note(self, crid): """https://familysearch.org/developers/docs/api/tree/Couple_Relationship_Note_resource""" + return self.couple_base + crid + "/notes/" + nid def couple_relationship_restore(self, cpid): """https://familysearch.org/developers/docs/api/tree/Couple_Relationship_Restore_resource""" - return self.couple_base + cpid + '/restore' \ No newline at end of file + + return self.couple_base + cpid + '/restore' diff --git a/familysearch/user.py b/familysearch/user.py index 5ae322f..cdde510 100644 --- a/familysearch/user.py +++ b/familysearch/user.py @@ -1,44 +1,51 @@ -"""FamilySearch User submodule""" -# Python imports +# -*- coding: utf-8 -*- +"""FamilySearch User submodule""" +# Python imports # Magic + class User(object): """https://familysearch.org/developers/docs/api/resources#user""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#user""" pass def current_user(self): """https://familysearch.org/developers/docs/api/users/Current_User_resource""" + url = self.root_collection['response']['collections'][0]['links']\ - ['current-user']['href'] + ['current-user']['href'] return url - + def current_user_person(self): """https://familysearch.org/developers/docs/api/tree/Current_Tree_Person_resource""" + try: url = self.collections["FSFT"]["response"]["collections"][0][ - "links"]["current-user-person"]["href"] + "links"]["current-user-person"]["href"] except KeyError: self.update_collection("FSFT") url = self.collections["FSFT"]["response"]["collections"][0][ - "links"]["current-user-person"]["href"] + "links"]["current-user-person"]["href"] return url def agent(self, uid): """https://familysearch.org/developers/docs/api/users/Agent_resource""" + return self.user_base + "agents/" + uid def current_user_history(self): """https://familysearch.org/developers/docs/api/users/Current_User_History_resource""" + try: url = self.collections["FSFT"]["response"]["collections"][0][ - "links"]["current-user-history"]["href"] + "links"]["current-user-history"]["href"] except KeyError: self.update_collection("FSFT") url = self.collections["FSFT"]["response"]["collections"][0][ - "links"]["current-user-history"]["href"] - return url \ No newline at end of file + "links"]["current-user-history"]["href"] + return url diff --git a/familysearch/utilities.py b/familysearch/utilities.py index ad4c4b9..9537671 100644 --- a/familysearch/utilities.py +++ b/familysearch/utilities.py @@ -1,25 +1,34 @@ +# -*- coding: utf-8 -*- + """FamilySearch Utilities submodule""" + # Python imports # Magic + class Utilities: """https://familysearch.org/developers/docs/api/resources#utilities""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#utilities""" + pass def pending_modifications(self): """https://familysearch.org/developers/docs/api/tree/Pending_Modifications_resource""" + return self.root_collection['response']['collections'][0]['links']\ ['pending-modifications']['href'] def redirect(self, **kwargs): """https://familysearch.org/developers/docs/api/tree/Redirect_resource""" + return self._add_query_params( self.base + "/platform/redirect", kwargs) - + def oembed(self, **kwargs): """https://familysearch.org/developers/docs/api/discovery/OEmbed_resource""" + return self._add_query_params( - self.base + "/platform/oembed", kwargs) \ No newline at end of file + self.base + "/platform/oembed", kwargs) diff --git a/familysearch/vocabularies.py b/familysearch/vocabularies.py index 31c451f..420abf5 100644 --- a/familysearch/vocabularies.py +++ b/familysearch/vocabularies.py @@ -1,16 +1,23 @@ +# -*- coding: utf-8 -*- + """FamilySearch Vocabularies submodule""" + # Python imports # Magic + class Vocabularies: """https://familysearch.org/developers/docs/api/resources#vocabularies""" + def __init__(self): """https://familysearch.org/developers/docs/api/examples#vocabularies""" + self.vocab_base = self.base + "platform/vocab/" def vocabulary_list(self, cvlid): """https://familysearch.org/developers/docs/api/cv/Controlled_Vocabulary_List_resource""" + try: url = self.collections["FSCV"]["response"]['collections'][ 0]['links']['vocab-list']['template'] @@ -22,9 +29,10 @@ def vocabulary_list(self, cvlid): shim["?access_token"] = "" shim["cvlid"] = cvlid return url.format(**shim) - + def vocabulary_lists(self): """https://familysearch.org/developers/docs/api/cv/Controlled_Vocabulary_List_resource""" + try: url = self.collections["FSCV"]["response"]['collections'][ 0]['links']['vocab-lists']['href'] @@ -36,8 +44,7 @@ def vocabulary_lists(self): def vocabulary_term(self, cvtid): """https://familysearch.org/developers/docs/api/cv/Controlled_Vocabulary_Term_resource""" - #return self.vocab_base + "lists/" + cvtid - """https://familysearch.org/developers/docs/api/cv/Controlled_Vocabulary_List_resource""" + try: url = self.collections["FSCV"]["response"]['collections'][ 0]['links']['vocab-term']['template'] diff --git a/sample-apps/web_person/exampleconfig.ini b/sample-apps/web_person/exampleconfig.ini index badd482..e49d2f9 100644 --- a/sample-apps/web_person/exampleconfig.ini +++ b/sample-apps/web_person/exampleconfig.ini @@ -1,10 +1,10 @@ +# Copy this file to config.ini and edit it. # Replace the value for devkey with your own dev key. # As per API rules, I cannot provide one. -# Then rename this file config.ini to run the sample apps. [fskey] devkey=MY-MAGIC-DEV-KEY-THAT-ALLOWS-FAMILY-SEARCH-ACCESS base=https://sandbox.familysearch.org [server] port=63342 -redirect_uri=http://localhost:63342/fspy \ No newline at end of file +redirect_uri=http://localhost:63342/fspy diff --git a/sample-apps/web_person/web-person.py b/sample-apps/web_person/web-person.py index a83e4f0..0b8650b 100644 --- a/sample-apps/web_person/web-person.py +++ b/sample-apps/web_person/web-person.py @@ -1,6 +1,10 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function import webbrowser import os import sys +import pprint try: # Python 3 @@ -12,78 +16,137 @@ import ConfigParser as configparser import BaseHTTPServer as server from urlparse import parse_qs - + from familysearch import FamilySearch config_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/config.ini" config = configparser.ConfigParser() config.read(config_path) -try: - app_key = config["fskey"]["devkey"] - base = config["fskey"]["base"] - port = config["server"]["port"] - redirect = config["server"]["redirect_uri"] -except AttributeError: - app_key = config.get("fskey", "devkey") - base = config.get("fskey", "base") - port = config.get("server", "port") - redirect = config.get("server", "redirect_uri") - -url = "http://localhost" + (":"+ port) if port is not "80" else "" +dev_key = config.get("fskey", "devkey") +base = config.get("fskey", "base") +port = config.get("server", "port") +redirect = config.get("server", "redirect_uri") + +url = "http://localhost" + (":" + port) if port is not "80" else "" ruri = "" for x in redirect[::-1]: ruri = x + ruri if x is "/": break -fs = FamilySearch("FSPySDK/SampleApps", app_key, base=base) +fs = FamilySearch("FSPySDK/SampleApps", dev_key, base=base) + +try: + fslogin = fs.root_collection["response"]['collections'][0]['links']\ + ['http://oauth.net/core/2.0/endpoint/authorize']['href'] +except KeyError as e: + print("KeyError:", str(e)) + raise + -fslogin = fs.root_collection['collections'][0]['links']\ - ['http://oauth.net/core/2.0/endpoint/authorize']['href'] +def qshow(): + def hr(): print("="*80) + hr() + print("fs.root_collection: ...") + pp = pprint.PrettyPrinter(width=120, indent=2) + pp.pprint(fs.root_collection) + hr() + print("""fs.root_collection["response"]: ...""") + pp.pprint(fs.root_collection["response"]) + hr() + print("""fs.root_collection["response"]["collections"]: ...""") + pp.pprint(fs.root_collection["response"]["collections"]) + hr() + print("""fs.root_collection["response"]["collections"][0]: ...""") + pp.pprint(fs.root_collection["response"]["collections"][0]) + hr() + print("""fs.root_collection["response"]["collections"][0]["links"]: ...""") + pp.pprint(fs.root_collection["response"]["collections"][0]["links"]) + hr() + print("""fs.root_collection["response"]["collections"][0]["links"]['http://oauth.net/core/2.0/endpoint/authorize']: ...""") + pp.pprint(fs.root_collection["response"]["collections"][0]["links"]['http://oauth.net/core/2.0/endpoint/authorize']) + hr() + print("""fs.root_collection["response"]["collections"][0]["links"]['http://oauth.net/core/2.0/endpoint/authorize']['href']: ...""") + pp.pprint(fs.root_collection["response"]["collections"][0]["links"]['http://oauth.net/core/2.0/endpoint/authorize']['href']) + hr() +# qshow() + +print("fslogin:", fslogin) +fslogin = fs._add_query_params(fslogin, { + 'response_type': 'code', + 'client_id': fs.dev_key, + 'redirect_uri': redirect + }) +print("fslogin:", fslogin) -fslogin = fs._add_query_params(fslogin, {'response_type': 'code', - 'client_id': fs.key, - 'redirect_uri': redirect - }) class getter(server.BaseHTTPRequestHandler): + def do_GET(self): self.send_response(code=200) - self.send_header("Content-type", "text/html") + self.send_header("Content-type", "text/html;charset=utf-8") self.end_headers() path = self.path - + print("path:", path) + top = "FSPySDK Sample App" + top += '' middle = "" bottom = "" if path.startswith(ruri): bottom = self.get_code(path) + bottom else: if fs.logged_in: - middle += logged_in() + middle += self.logged_in() if path.startswith("/?pid="): pid = parse_qs(path)["/?pid"][0] - person = fs.get_person(pid) - middle += has_pid(person) + person = fs.get(fs.person(pid)) # = fs.get_person(pid) + middle += self.has_pid(person) else: middle = self.not_logged_in() + middle middle = self.not_logged_in() - body = top + middle + bottom - + body = top + middle + bottom + self.wfile.write(body.encode("utf-8")) def not_logged_in(self): string = '' + string += '","fsWindow","width=560,height=632");}' string += "" string += "" return string + def logged_in(self): + def show_fs(): + print("="*80) + pp = pprint.PrettyPrinter(width=120, indent=2) + for k in sorted(list(fs.__dict__.keys())): + if k[0] == '_': continue + v = fs.__dict__[k] + vt = type(v) + if isinstance(v, (bool, type(''), type(b''), type(u''), tuple, list, set, frozenset)): + print(k, vt, v) + elif isinstance(v, (dict,)): + print(k, vt, "...") + pp.pprint(v) + else: + print(k, vt) + print("") + print("="*80) + + def p(): + show_fs() + print("fs.user:", type(fs.user), fs.user) + print("fs.current_user():", fs.current_user()) + print("fs.current_user_person():", fs.current_user_person()) + print("fs.agent('x'):", fs.agent('x')) + print("fs.current_user_history():", fs.current_user_history()) + p() string = 'Search given FamilySearch PID (default is your own)
' - string +='' if person['response']['persons'][0]['display']['gender'] == "Male": - string += 'He' - else: + string += 'He' + else: string += 'She' string += " is " if person['persons'][0]['living']: @@ -103,8 +166,8 @@ def has_pid(self, person): if person['response']['persons'][0]['display']['gender'] == "Male": string += 'His' else: - string +="Her" - string +=' lifespan is "' + string += "Her" + string += ' lifespan is "' string += person['persons'][0]['display']['lifespan'] string += '".
' return string @@ -116,4 +179,4 @@ def get_code(self, path): return '' webbrowser.open(url) -server.HTTPServer(('', 63342), getter).serve_forever() \ No newline at end of file +server.HTTPServer(('', int(port)), getter).serve_forever() diff --git a/sample-apps/whoami/exampleconfig.ini b/sample-apps/whoami/exampleconfig.ini index e30af4b..743ddba 100644 --- a/sample-apps/whoami/exampleconfig.ini +++ b/sample-apps/whoami/exampleconfig.ini @@ -1,6 +1,6 @@ +# Copy this file to config.ini and edit it. # Replace the value for devkey with your own dev key. # As per API rules, I cannot provide one. -# Then rename this file config.ini to run the sample apps. [fskey] devkey=MY-MAGIC-DEV-KEY-THAT-ALLOWS-FAMILY-SEARCH-ACCESS base=https://sandbox.familysearch.org diff --git a/sample-apps/whoami/whoami.py b/sample-apps/whoami/whoami.py index cb14203..bacf898 100644 --- a/sample-apps/whoami/whoami.py +++ b/sample-apps/whoami/whoami.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # Python imports from __future__ import print_function import os @@ -18,26 +20,60 @@ # Get app key from config.ini -config_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/config.ini" +def get_config(): + config_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/config.ini" -config = configparser.ConfigParser() -config.read(config_path) -try: - app_key = config["fskey"]["devkey"] - base = config["fskey"]["base"] -except AttributeError: - app_key = config.get("fskey", "devkey") - base = config.get("fskey", "base") + config = configparser.ConfigParser() + config.read(config_path) + return config + +def use_login(fs, config): + # keep on trying until successful. + user = config.get("fskey", "user") + password = config.get("fskey", "password") + if user and password: + print("Try user/password from config.ini") + try: + fs.login(user, password) + except HTTPError as e: + print("HTTPError:", str(e)) + # print("dir(e):", dir(e)) + print("code:", e.code) + print("hdrs:", str(e.hdrs).replace("\n", "\n\t")) + print("reason:", e.reason) + print("Not logged in.") + finally: + user = password = None + + if not fs.logged_in: + print("Please sign in to FamilySearch.") -# Sign into FamilySearch, and keep on trying until successful. + while not fs.logged_in: + try: + user = input("Username: ") + password = getpass() + fs.login(user, password) + except HTTPError: + print("Not logged in. Try again.") + except EOFError: + exit(1) + finally: + user = password = None + +def use_desktop_login(fs): + ruri = "http://localhost:63342/fspy" + fs.oauth_desktop_login(ruri) + +config = get_config() +app_key = config.get("fskey", "devkey") +base = config.get("fskey", "base") + +# Sign into FamilySearch fs = FamilySearch("FSPySDK/SampleApps", app_key, base=base) -print("Please sign in to FamilySearch.") -while fs.logged_in is not True: - try: - fs.login(input("Username: "), getpass()) - except HTTPError: - print("Not logged in. Try again.") + +#use_login(fs, config) +use_desktop_login(fs) me = fs.get_current_user()['response']['users'][0]['displayName'] -print("Welcome, " + me + "!") \ No newline at end of file +print("Welcome, " + me + "!") diff --git a/setup.py b/setup.py index 23d207f..b91b9a7 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + try: # Setuptools or Distribute is required to support running `python setup.py test` from setuptools import setup @@ -15,11 +17,11 @@ version = version, description = 'A Python SDK for FamilySearch.org', long_description=open('README.rst').read(), - author = 'Elder Evans', - author_email = 'elderamevans@gmail.com', - url = 'https://github.com/elderamevans/familysearch-python-sdk-opensource', - download_url = 'https://github.com/elderamevans/familysearch-python-sdk-opensource/releases/tag/v' + version, - keywords = ['FamilySearch', 'API', 'REST', 'family history', 'geneaolgy', 'JSON'], + author = 'Doug Henderson, Elder Evans, Peter Henderson', + author_email = 'djhenderson on github.com', + url = 'https://github.com/djhenderson/familysearch-python-sdk-opensource', + download_url = 'https://github.com/djhenderson/familysearch-python-sdk-opensource/releases/tag/v' + version, + keywords = ['FamilySearch', 'API', 'REST', 'family history', 'genealogy', 'JSON'], classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -36,6 +38,7 @@ "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", "Topic :: Internet :: WWW/HTTP", "Topic :: Sociology :: Genealogy", "Topic :: Software Development :: Libraries :: Python Modules" diff --git a/test/exampleconfig.ini b/test/exampleconfig.ini index c3f45d6..7feac44 100644 --- a/test/exampleconfig.ini +++ b/test/exampleconfig.ini @@ -1,4 +1,6 @@ +# Copy this file to config.ini and edit it. # Replace the values for user, password and devkey with your own valid values. +# As per API rules, I cannot provide one. [fsTest] user=myusername password=mypassword diff --git a/test/run_tests.py b/test/run_tests.py index d9bc157..f5231ff 100644 --- a/test/run_tests.py +++ b/test/run_tests.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- + +from __future__ import print_function from test.test_basefs import TestFamilySearch -#from test.test_enhanced_request import TestEnhancedRequest +from test.test_enhanced_request import TestEnhancedRequest fs = TestFamilySearch() fs.runTest() # Removing until we can get it platform-independent... -#er = TestEnhancedRequest() -#er.runTest() +er = TestEnhancedRequest() +er.runTest() diff --git a/test/test_basefs.py b/test/test_basefs.py index 06286bf..b641b14 100644 --- a/test/test_basefs.py +++ b/test/test_basefs.py @@ -1,8 +1,11 @@ # -*- coding: utf-8 -*- + """ Test the base familysearch module contained in __init__.py """ + # import system modules +from __future__ import print_function import json try: from urllib import request @@ -15,9 +18,10 @@ # import familysearch module import familysearch + class TestFamilySearch(util.FSTemplateTest): """Test the base familysearch module contained in __init__.py""" - + def runTest(self): self.setUp() self.test_base_fs_creation() @@ -36,10 +40,9 @@ def test_base_fs_creation(self): fs = familysearch.FamilySearch(self.agent, self.devkey) self.assertTrue(fs.base == 'https://sandbox.familysearch.org') print("Base is correct.") - self.assertTrue(fs.key == self.devkey) + self.assertTrue(fs.dev_key == self.devkey) print("Key is correct.") self.assertTrue(isinstance(fs.opener, request.OpenerDirector)) print("HTTP opener works.") - self.assertTrue(fs.access_token == None) + self.assertIsNone(fs.access_token) print("Access token works.") - diff --git a/test/test_enhanced_request.py b/test/test_enhanced_request.py index 54b9311..6cb8569 100644 --- a/test/test_enhanced_request.py +++ b/test/test_enhanced_request.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- + """ Test the EnhancedRequest object contained in __init__.py """ + # import system modules +from __future__ import print_function, unicode_literals import json +import pprint # Python imports try: # Python 3 @@ -24,56 +28,98 @@ BASE_TEST_URL = "http://httpbin.org" + class TestEnhancedRequest(util.FSTemplateTest): """Test the EnhancedRequest object contained in __init__.py""" + + def runTest(self): + self.setUp() + self.test_delete() + self.test_get() + self.test_head() + self.test_options() + self.test_post() + self.test_put() + self.tearDown() + # setup def setUp(self): util.FSTemplateTest.setUp(self) self.base_url = BASE_TEST_URL - # teardown def tearDown(self): util.FSTemplateTest.tearDown(self) + @staticmethod + def _hr(): + print("="*80) + + def _show_in_out(self, actual, expected, msg_actual, msg_expected): + + pp = pprint.PrettyPrinter(indent=2, width=120) + + if isinstance(expected, dict): + print(msg_actual, type(actual), "...") + pp.pprint(actual) + print(msg_expected, type(expected), "...") + pp.pprint(expected) + + elif isinstance(expected, list): + print(msg_actual, type(actual), "...") + pp.pprint(actual) + print(msg_expected, type(expected), "...") + pp.pprint(expected) + else: + print("actual:", type(actual), "expected:", type(expected)) + raise TypeError + + self._hr() def test_delete(self): expected = { 'args': {}, 'form': {}, - 'origin': '192.168.1.1', + 'origin': '192.168.1.70', 'headers': { - 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', + # 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', 'Accept-Encoding': 'identity', - 'User-Agent': 'Python-urllib/3.4', + 'User-Agent': 'Python-urllib/3.5', 'Host': 'httpbin.org', - 'Connection': 'close' + # 'Connection': 'close', }, 'data': '', 'files': {}, 'url': 'http://httpbin.org/delete', - 'json': None + 'json': None, } url = self.base_url + '/delete' request = Request(url, method='DELETE') response = urlopen(request) actual = json.loads(response.read().decode('utf-8')) self.assertTrue(actual) - self.assertTrue(actual.keys() == expected.keys()) + actual_keys = list(actual.keys()) + actual_keys.sort() + expected_keys = list(expected.keys()) + expected_keys.sort() + # self._hr() + # self._show_in_out(actual_keys, expected_keys, "actual keys", "expected keys") + self.assertTrue(actual_keys == expected_keys) + # self._hr() + # self._show_in_out(actual.get('headers', {}).keys(), expected['headers'].keys(), "actual[headers]", "expected[headers]") self.assertTrue(actual.get('headers', {}).keys() == - expected['headers'].keys()) - + expected['headers'].keys()) def test_get(self): expected = { 'args': {}, - 'origin': '192.168.1.1', + 'origin': '192.168.1.70', 'headers': { - 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', + # 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', 'Accept-Encoding': 'identity', - 'User-Agent': 'Python-urllib/3.4', + 'User-Agent': 'Python-urllib/3.5', 'Host': 'httpbin.org', - 'Connection': 'close' + # 'Connection': 'close', }, 'url': 'http://httpbin.org/get', } @@ -84,70 +130,78 @@ def test_get(self): self.assertTrue(actual) self.assertTrue(actual.keys() == expected.keys()) self.assertTrue(actual.get('headers', {}).keys() == - expected['headers'].keys()) - + expected['headers'].keys()) def test_head(self): expected = [ - ('Access-Control-Allow-Credentials', 'true'), - ('Access-Control-Allow-Origin', '*'), - ('Content-length', '294'), - ('Content-Type', 'application/json'), - ('Date', 'Thu, 18 Sep 2014 00:29:57 GMT'), - ('Server', 'gunicorn/18.0'), - ('Connection', 'Close') + ('access-control-allow-credentials', 'true'), + ('access-control-allow-origin', '*'), + ('content-length', '294'), + ('content-type', 'application/json'), + ('date', 'Thu, 18 Sep 2014 00:29:57 GMT'), + ('server', 'gunicorn/18.0'), + ('connection', 'Close') ] url = self.base_url + '/get' request = Request(url, method='HEAD') response = urlopen(request) - actual = response.getheaders() - expected_fields = [value[0] for value in expected] - actual_fields = [value[0] for value in actual] + # print("dir(response):", dir(response)) + # print("headers:", type(response.headers), response.headers.__class__) + # print(response.headers.__class__, dir(response.headers)) + actual = response.headers self.assertTrue(actual) - self.assertTrue(actual_fields == expected_fields) - + expected_keys = [k for k, v in expected] + actual_keys = [k.lower() for k in actual.keys()] + expected_keys.sort() + actual_keys.sort() + # self._hr() + # self._show_in_out(actual_keys, expected_keys, "actual keys", "expected keys") + self.assertTrue(actual_keys == expected_keys) def test_options(self): expected = [ - ('Access-Control-Allow-Credentials', 'true'), - ('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'), - ('Access-Control-Allow-Origin', '*'), - ('Access-Control-Max-Age', '3600'), - ('Allow', 'GET, HEAD, OPTIONS'), - ('Content-Type', 'text/html; charset=utf-8'), - ('Date', 'Thu, 18 Sep 2014 00:29:57 GMT'), - ('Server', 'gunicorn/18.0'), - ('Content-Length', '0'), - ('Connection', 'Close') + ('access-control-allow-credentials', 'true'), + ('access-control-allow-methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS'), + ('access-control-allow-origin', '*'), + ('access-control-max-age', '3600'), + ('allow', 'GET, HEAD, OPTIONS'), + ('content-type', 'text/html; charset=utf-8'), + ('date', 'Thu, 18 Sep 2014 00:29:57 GMT'), + ('server', 'gunicorn/18.0'), + ('content-length', '0'), + ('connection', 'Close') ] url = self.base_url request = Request(url, method='OPTIONS') response = urlopen(request) - actual = response.getheaders() - expected_fields = [value[0] for value in expected] - actual_fields = [value[0] for value in actual] + actual = response.headers self.assertTrue(actual) - self.assertTrue(actual_fields == expected_fields) - + expected_keys = [k for k, v in expected] + actual_keys = [k.lower() for k in actual.keys()] + expected_keys.sort() + actual_keys.sort() + # self._hr() + # self._show_in_out(actual_keys, expected_keys, "actual keys", "expected keys") + self.assertTrue(actual_keys == expected_keys) def test_post(self): expected = { 'args': {}, 'form': {'spam': '1', 'eggs': '2', 'bacon': '3'}, - 'origin': '192.168.1.1', + 'origin': '192.168.1.70', 'headers': { 'Content-Length': '21', - 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', + # 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', 'Accept-Encoding': 'identity', 'Content-Type': 'application/x-www-form-urlencoded', - 'User-Agent': 'Python-urllib/3.4', + 'User-Agent': 'Python-urllib/3.5', 'Host': 'httpbin.org', - 'Connection': 'close' + # 'Connection': 'close', }, 'data': '', 'files': {}, 'url': 'http://httpbin.org/delete', - 'json': None + 'json': None, } url = self.base_url + '/post' data_dict = {'spam': 1, 'eggs': 2, 'bacon': 3} @@ -157,24 +211,34 @@ def test_post(self): response = urlopen(request) actual = json.loads(response.read().decode('utf-8')) self.assertTrue(actual) - self.assertTrue(actual.keys() == expected.keys()) - self.assertTrue(actual.get('headers', {}).keys() == - expected['headers'].keys()) + actual_keys = list(actual.keys()) + expected_keys = list(expected.keys()) + actual_keys.sort() + expected_keys.sort() + # self._hr() + # self._show_in_out(actual_keys, expected_keys, "actual keys", "expected keys") + self.assertTrue(actual_keys == expected_keys) + akeys = list(actual.get('headers', {}).keys()) + ekeys = list(expected['headers'].keys()) + akeys.sort() + ekeys.sort() + # self._hr() + # self._show_in_out(akeys, ekeys, "actual head keys", "expected head keys") + self.assertTrue(akeys == ekeys) self.assertTrue(actual.get('form', None) == expected['form']) - def test_put(self): expected = { 'args': {}, 'form': {}, - 'origin': '192.168.1.1', + 'origin': '192.168.1.70', 'headers': { 'Content-Length': '0', - 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', + # 'X-Request-Id': 'e4f0cb78-afc7-4617-aac1-a0c13fa746cc', 'Accept-Encoding': 'identity', - 'User-Agent': 'Python-urllib/3.4', + 'User-Agent': 'Python-urllib/3.5', 'Host': 'httpbin.org', - 'Connection': 'close' + # 'Connection': 'close' }, 'data': '', 'files': {}, @@ -186,7 +250,17 @@ def test_put(self): response = urlopen(request) actual = json.loads(response.read().decode('utf-8')) self.assertTrue(actual) - self.assertTrue(actual.keys() == expected.keys()) - self.assertTrue(actual.get('headers', {}).keys() == - expected['headers'].keys()) - + akeys = list(actual.keys()) + ekeys = list(expected.keys()) + akeys.sort() + ekeys.sort() + self._hr() + self._show_in_out(akeys, ekeys, "actual keys", "expected keys") + self.assertTrue(akeys == ekeys) + akeys = list(actual["headers"].keys()) + ekeys = list(expected["headers"].keys()) + akeys.sort() + ekeys.sort() + self._hr() + self._show_in_out(akeys, ekeys, "actual head keys", "expected head keys") + self.assertTrue(akeys == ekeys) diff --git a/test/util.py b/test/util.py index 4306ad4..5c1a3a1 100644 --- a/test/util.py +++ b/test/util.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- + """Utilities to assist in testing.""" # import system modules +from __future__ import print_function import inspect import os import sys @@ -28,6 +30,7 @@ def get_config(): config.read(config_path) return config + class FSTemplateTest(unittest.TestCase): """FSTemplateTest is the base class for familysearch tests. """ @@ -36,15 +39,15 @@ def setUp(self): unittest.TestCase.setUp(self) self.config = get_config() try: - #Python 2 + # Python 2 self.user = self.config.get('fsTest', 'user') self.password = self.config.get('fsTest', 'password') - self.devkey = self.config.get('fsTest', 'devkey') + self.devkey = self.config.get('fsTest', 'devkey') except AttributeError: # Python 3 self.devkey = self.config["fsTest"]["devkey"] - self.username = self.config["fsTest"]["username"] - self.username = self.config["fsTest"]["password"] + self.user = self.config["fsTest"]["username"] + self.password = self.config["fsTest"]["password"] # common teardown def tearDown(self):