def test_fetch_search_index(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing search indexes. """ ddoc = DesignDocument(self.db, '_design/ddoc001') search_index = ('function (doc) {\n index("default", doc._id); ' 'if (doc._id) {index("name", doc.name, ' '{"store": true}); }\n} ') ddoc.add_search_index('search001', search_index) ddoc.add_search_index('search002', search_index, 'simple') ddoc.add_search_index('search003', search_index, 'standard') ddoc.save() ddoc_remote = DesignDocument(self.db, '_design/ddoc001') self.assertNotEqual(ddoc_remote, ddoc) ddoc_remote.fetch() self.assertEqual(ddoc_remote, ddoc) self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote, { '_id': '_design/ddoc001', '_rev': ddoc['_rev'], 'indexes': { 'search001': {'index': search_index}, 'search002': {'index': search_index, 'analyzer': 'simple'}, 'search003': {'index': search_index, 'analyzer': 'standard'} }, 'views': {} })
def test_fetch_query_views(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { "_id": "_design/ddoc001", "language": "query", "views": { "view001": { "map": {"fields": {"name": "asc", "age": "asc"}}, "reduce": "_count", "options": {"def": {"fields": ["name", "age"]}, "w": 2}, } }, } doc = self.db.create_document(data) self.assertIsInstance(doc, Document) data["_rev"] = doc["_rev"] ddoc = DesignDocument(self.db, "_design/ddoc001") ddoc.fetch() self.assertIsInstance(ddoc, DesignDocument) self.assertEqual(ddoc, data) self.assertIsInstance(ddoc["views"]["view001"], QueryIndexView)
def test_cq_view_save_fails_when_lang_is_not_query(self): """ Tests that save fails when language is not query but views are query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { '_id': '_design/ddoc001', 'language': 'query', 'views': { 'view001': {'map': {'fields': {'name': 'asc', 'age': 'asc'}}, 'reduce': '_count', 'options': {'def': {'fields': ['name', 'age']}, 'w': 2} } } } self.db.create_document(data) ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.fetch() with self.assertRaises(CloudantException) as cm: ddoc['language'] = 'not-query' ddoc.save() err = cm.exception self.assertEqual(str(err), 'View view001 must be of type View.') with self.assertRaises(CloudantException) as cm: del ddoc['language'] ddoc.save() err = cm.exception self.assertEqual(str(err), 'View view001 must be of type View.')
def test_fetch_query_views(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { '_id': '_design/ddoc001', 'language': 'query', 'views': { 'view001': {'map': {'fields': {'name': 'asc', 'age': 'asc'}}, 'reduce': '_count', 'options': {'def': {'fields': ['name', 'age']}, 'w': 2} } } } doc = self.db.create_document(data) self.assertIsInstance(doc, Document) data['_rev'] = doc['_rev'] ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.fetch() self.assertIsInstance(ddoc, DesignDocument) self.assertEqual(ddoc, data) self.assertIsInstance(ddoc['views']['view001'], QueryIndexView)
def test_fetch_text_indexes(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { '_id': '_design/ddoc001', 'language': 'query', 'indexes': {'index001': {'index': {'index_array_lengths': True, 'fields': [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}], 'default_field': {'enabled': True, 'analyzer': 'german'}, 'default_analyzer': 'keyword', 'selector': {}}, 'analyzer': {'name': 'perfield', 'default': 'keyword', 'fields': {'$default': 'german'}}}}} doc = self.db.create_document(data) self.assertIsInstance(doc, Document) data['_rev'] = doc['_rev'] ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.fetch() self.assertIsInstance(ddoc, DesignDocument) self.assertEqual(ddoc, data) self.assertIsInstance(ddoc['indexes']['index001'], dict)
def test_fetch_map_reduce(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing MapReduce views. """ ddoc = DesignDocument(self.db, "_design/ddoc001") view_map = "function (doc) {\n emit(doc._id, 1);\n}" view_reduce = "_count" db_copy = "{0}-copy".format(self.db.database_name) ddoc.add_view("view001", view_map, view_reduce) ddoc.add_view("view002", view_map, view_reduce, dbcopy=db_copy) ddoc.add_view("view003", view_map) ddoc.save() ddoc_remote = DesignDocument(self.db, "_design/ddoc001") self.assertNotEqual(ddoc_remote, ddoc) ddoc_remote.fetch() self.assertEqual(ddoc_remote, ddoc) self.assertTrue(ddoc_remote["_rev"].startswith("1-")) self.assertEqual( ddoc_remote, { "_id": "_design/ddoc001", "_rev": ddoc["_rev"], "views": { "view001": {"map": view_map, "reduce": view_reduce}, "view002": {"map": view_map, "reduce": view_reduce, "dbcopy": db_copy}, "view003": {"map": view_map}, }, }, ) self.assertIsInstance(ddoc_remote["views"]["view001"], View) self.assertIsInstance(ddoc_remote["views"]["view002"], View) self.assertIsInstance(ddoc_remote["views"]["view003"], View)
def test_fetch_map_reduce(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing MapReduce views. """ ddoc = DesignDocument(self.db, '_design/ddoc001') view_map = 'function (doc) {\n emit(doc._id, 1);\n}' view_reduce = '_count' db_copy = '{0}-copy'.format(self.db.database_name) ddoc.add_view('view001', view_map, view_reduce) ddoc.add_view('view002', view_map, view_reduce, dbcopy=db_copy) ddoc.add_view('view003', view_map) ddoc.save() ddoc_remote = DesignDocument(self.db, '_design/ddoc001') self.assertNotEqual(ddoc_remote, ddoc) ddoc_remote.fetch() self.assertEqual(ddoc_remote, ddoc) self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote, { '_id': '_design/ddoc001', '_rev': ddoc['_rev'], 'views': { 'view001': {'map': view_map, 'reduce': view_reduce}, 'view002': {'map': view_map, 'reduce': view_reduce, 'dbcopy': db_copy}, 'view003': {'map': view_map} } }) self.assertIsInstance(ddoc_remote['views']['view001'], View) self.assertIsInstance(ddoc_remote['views']['view002'], View) self.assertIsInstance(ddoc_remote['views']['view003'], View)
def test_query_view_save_fails_when_lang_is_not_query(self): """ Tests that save fails when language is not query but views are query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { "_id": "_design/ddoc001", "language": "query", "views": { "view001": { "map": {"fields": {"name": "asc", "age": "asc"}}, "reduce": "_count", "options": {"def": {"fields": ["name", "age"]}, "w": 2}, } }, } self.db.create_document(data) ddoc = DesignDocument(self.db, "_design/ddoc001") ddoc.fetch() with self.assertRaises(CloudantException) as cm: ddoc["language"] = "not-query" ddoc.save() err = cm.exception self.assertEqual(str(err), "View view001 must be of type View.") with self.assertRaises(CloudantException) as cm: del ddoc["language"] ddoc.save() err = cm.exception self.assertEqual(str(err), "View view001 must be of type View.")
def test_fetch_text_indexes(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected when retrieving a design document containing query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { "_id": "_design/ddoc001", "language": "query", "indexes": { "index001": { "index": { "index_array_lengths": True, "fields": [{"name": "name", "type": "string"}, {"name": "age", "type": "number"}], "default_field": {"enabled": True, "analyzer": "german"}, "default_analyzer": "keyword", "selector": {}, }, "analyzer": {"name": "perfield", "default": "keyword", "fields": {"$default": "german"}}, } }, } doc = self.db.create_document(data) self.assertIsInstance(doc, Document) ddoc = DesignDocument(self.db, "_design/ddoc001") ddoc.fetch() self.assertIsInstance(ddoc, DesignDocument) data["_rev"] = doc["_rev"] data["views"] = dict() self.assertEqual(ddoc, data) self.assertIsInstance(ddoc["indexes"]["index001"], dict)
def get(self, key, default=None, remote=False): """ Overrides dictionary __getitem__ behavior to provide a document instance for the specified key from the current database. If the document instance does not exist locally, then a remote request is made and the document is subsequently added to the local cache and returned to the caller. If the document instance already exists locally then it is returned and a remote request is not performed. A KeyError will result if the document does not exist locally or in the remote database. :param str key: Document id used to retrieve the document from the database. :returns: A Document or DesignDocument object depending on the specified document id (key) """ if remote is False: if key in list(self.keys()): return super(CouchDatabase, self).__getitem__(key) if key.startswith('_design/'): doc = DesignDocument(self, key) else: doc = Document(self, key) if doc.exists(): doc.fetch() super(CouchDatabase, self).__setitem__(key, doc) return doc return default
def test_view_callable_with_invalid_javascript(self): """ Test error condition when Javascript errors exist. This test is only valid for CouchDB because the map function Javascript is validated on the Cloudant server when attempting to save a design document so invalid Javascript is not possible there. """ self.populate_db_with_documents() ddoc = DesignDocument(self.db, 'ddoc001') ddoc.add_view( 'view001', 'This is not valid Javascript' ) ddoc.save() # Verify that the ddoc and view were saved remotely # along with the invalid Javascript del ddoc ddoc = DesignDocument(self.db, 'ddoc001') ddoc.fetch() view = ddoc.get_view('view001') self.assertEqual(view.map, 'This is not valid Javascript') try: for row in view.result: self.fail('Above statement should raise an Exception') except requests.HTTPError as err: self.assertEqual(err.response.status_code, 500)
def test_fetch(self): """ Ensure that the document fetch from the database returns the DesignDocument format as expected. """ ddoc = DesignDocument(self.db, '_design/ddoc001') view_map = 'function (doc) {\n emit(doc._id, 1);\n}' view_reduce = '_count' ddoc.add_view('view001', view_map) ddoc.add_view('view002', view_map, view_reduce) ddoc.add_view('view003', view_map) ddoc.save() ddoc_remote = DesignDocument(self.db, '_design/ddoc001') self.assertNotEqual(ddoc_remote, ddoc) ddoc_remote.fetch() self.assertEqual(ddoc_remote, ddoc) self.assertEqual(len(ddoc_remote['views']), 3) reduce_count = 0 for x in xrange(1, 4): name = 'view{0:03d}'.format(x) view = ddoc_remote['views'][name] self.assertIsInstance(view, View) self.assertEqual(view.map, view_map) if name == 'view002': reduce_count += 1 self.assertEqual(view.reduce, view_reduce) else: self.assertIsNone(view.reduce) self.assertEqual(reduce_count, 1)
def test_text_index_save_fails_with_existing_search_index(self): """ Tests that save fails when language is not query and both a query text index and a search index exist in the design document. """ ddoc = DesignDocument(self.db, '_design/ddoc001') search_index = ('function (doc) {\n index("default", doc._id); ' 'if (doc._id) {index("name", doc.name, ' '{"store": true}); }\n}') ddoc.add_search_index('search001', search_index) self.assertIsInstance( ddoc['indexes']['search001']['index'], str ) ddoc.save() self.assertTrue(ddoc['_rev'].startswith('1-')) ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() ddoc_remote['indexes']['index001'] = { 'index': {'index_array_lengths': True, 'fields': [{'name': 'name', 'type': 'string'}, {'name': 'age', 'type': 'number'}], 'default_field': {'enabled': True, 'analyzer': 'german'}, 'default_analyzer': 'keyword', 'selector': {}}, 'analyzer': {'name': 'perfield','default': 'keyword', 'fields': {'$default': 'german'}}} self.assertIsInstance(ddoc_remote['indexes']['index001']['index'], dict) with self.assertRaises(CloudantException) as cm: ddoc_remote.save() err = cm.exception self.assertEqual( str(err), 'Function for search index index001 must be of type string.' )
def test_delete_a_query_index_view(self): """ Test deleting a query index view fails as expected. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { '_id': '_design/ddoc001', 'language': 'query', 'views': { 'view001': {'map': {'fields': {'name': 'asc', 'age': 'asc'}}, 'reduce': '_count', 'options': {'def': {'fields': ['name', 'age']}, 'w': 2} } } } self.db.create_document(data) ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.fetch() with self.assertRaises(CloudantException) as cm: ddoc.delete_view('view001') err = cm.exception self.assertEqual( str(err), 'Cannot delete a query index view using this method.' )
def create_summaries(conn): """ Get summary statistics from a view in the sentiments db and update the summaries db. """ # query view logging.info('Querying database for summary...') db = conn['sentiments'] ddoc = DesignDocument(db, '_design/summary') ddoc.fetch() view = ddoc.get_view('summary-view') view_results = view(group=True)['rows'] logging.info('Query completed.') # switch to summaries db db = conn['summaries'] for view_result in view_results: symbol = view_result['key'] unix_date = int(time.mktime(date.today().timetuple())) # query for today's summary record query = Query(db, fields=['_id', 'symbol', 'date', 'summary'], selector={ 'symbol': { '$eq': symbol }, 'date': { '$eq': unix_date } }) if query.result[0]: # A record for today already exists so overwrite it. This should not normally happen. record = db[query.result[0][0]['_id']] summary = record['summary'] logging.info('Updating summary for %s', symbol) record['summary'] = view_result['value'] summary.save() else: # Creating a new summary logging.info('Creating summary for %s...', symbol) data = { 'symbol': symbol, 'date': unix_date, 'summary': view_result['value'] } new_summary = db.create_document(data) if new_summary.exists(): logging.info('Finished creating summary.') else: logging.error('Failed to create summary.') # don't exceed rate limit time.sleep(1)
def get_transactions(self, page, pagesize): print("Portfolio.get_transactions()") skip = page * pagesize ddoc = DesignDocument(self.db, 'transactions') ddoc.fetch() view = View(ddoc, 'history') return view(include_docs=False, limit=pagesize, skip=skip, reduce=False)['rows']
def test_fetch_existing_design_document_with_docid_encoded_url(self): """ Test fetching design document content from an existing document where the document id requires an encoded url """ ddoc = DesignDocument(self.db, "_design/http://example.com") ddoc.create() new_ddoc = DesignDocument(self.db, "_design/http://example.com") new_ddoc.fetch() self.assertEqual(new_ddoc, ddoc)
def raw_view(self, view_path, params): params = deepcopy(params) params.pop('dynamic_properties', None) if view_path == '_all_docs': return self.cloudant_database.all_docs(**params) else: view_path = view_path.split('/') assert len(view_path) == 4 ddoc = DesignDocument(self.cloudant_database, view_path[1]) ddoc.fetch() view = ddoc.get_view(view_path[3]) return view(**params)
def test_update_design_document_with_encoded_url(self): """ Test that updating a design document where the document id requires that the document url be encoded is successful. """ # First create the design document ddoc = DesignDocument(self.db, "_design/http://example.com") ddoc.save() # Now test that the design document gets updated ddoc.save() self.assertTrue(ddoc["_rev"].startswith("2-")) remote_ddoc = DesignDocument(self.db, "_design/http://example.com") remote_ddoc.fetch() self.assertEqual(remote_ddoc, ddoc)
def __getitem__(self, key): if not self.remote: if key in list(self.keys(remote=False)): return super(CouchDatabase, self).__getitem__(key) if key.startswith("_design/"): doc = DesignDocument(self, key) else: doc = Document(self, key) if doc.exists(): doc.fetch() super(CouchDatabase, self).__setitem__(key, doc) return doc else: raise KeyError(key)
def test_fetch_no_views(self): """ Ensure that the document fetched from the database returns the DesignDocument format as expected when retrieving a design document containing no views. """ ddoc = DesignDocument(self.db, "_design/ddoc001") ddoc.save() ddoc_remote = DesignDocument(self.db, "_design/ddoc001") ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), {"_id", "_rev", "views"}) self.assertEqual(ddoc_remote["_id"], "_design/ddoc001") self.assertTrue(ddoc_remote["_rev"].startswith("1-")) self.assertEqual(ddoc_remote["_rev"], ddoc["_rev"]) self.assertEqual(ddoc_remote.views, {})
def test_fetch_no_views(self): """ Ensure that the document fetched from the database returns the DesignDocument format as expected when retrieving a design document containing no views. """ ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.save() ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), {'_id', '_rev', 'indexes', 'views'}) self.assertEqual(ddoc_remote['_id'], '_design/ddoc001') self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote['_rev'], ddoc['_rev']) self.assertEqual(ddoc_remote.views, {})
def test_design_doc(self, mock_fetch): """test overridden methods work as expected""" mock_database = mock.Mock() ddoc = DesignDocument(mock_database, '_design/unittest') ddoc['views'] = { 'view1': {'map': "MAP", 'reduce': 'REDUCE'} } ddoc.fetch() self.assertTrue(mock_fetch.called) views = [x for x in ddoc.iterviews()] self.assertEqual(len(views), 1) view = views[0] self.assertIn('view1', view) funcs = view[1] self.assertEqual(funcs['map'], 'MAP') self.assertEqual(funcs['reduce'], 'REDUCE') self.assertIn('view1', ddoc.views)
def create_latest_recommendations_index(): ddoc_fn = ''' function(doc) { emit([doc.user, doc.timestamp], null); } ''' db = cloudant_client[CL_RECOMMENDDB] index_name = 'latest-recommendation-index' ddoc = DesignDocument(db, index_name) if ddoc.exists(): ddoc.fetch() ddoc.update_view(index_name, ddoc_fn) print('updated', index_name) else: ddoc.add_view(index_name, ddoc_fn) print('created', index_name) ddoc.save()
def test_fetch_no_search_index(self): """ Ensure that the document fetched from the database returns the DesignDocument format as expected when retrieving a design document containing no search indexes. The :func:`~cloudant.design_document.DesignDocument.fetch` function adds the ``indexes`` key in the locally cached DesignDocument if indexes do not exist in the remote design document. """ ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.save() ddoc_remote = DesignDocument(self.db, '_design/ddoc001') ddoc_remote.fetch() self.assertEqual(set(ddoc_remote.keys()), {'_id', '_rev', 'indexes', 'views'}) self.assertEqual(ddoc_remote['_id'], '_design/ddoc001') self.assertTrue(ddoc_remote['_rev'].startswith('1-')) self.assertEqual(ddoc_remote['_rev'], ddoc['_rev']) self.assertEqual(ddoc_remote.indexes, {})
def create_moviedb_indexes(): ddoc_fn = ''' function(doc){ index("default", doc._id); if (doc.name){ index("name", doc.name, {"store": true}); } } ''' db = cloudant_client[CL_MOVIEDB] index_name = 'movie-search-index' ddoc = DesignDocument(db, index_name) if ddoc.exists(): ddoc.fetch() ddoc.update_search_index(index_name, ddoc_fn, analyzer=None) print('updated', index_name) else: ddoc.add_search_index(index_name, ddoc_fn, analyzer=None) print('created', index_name) ddoc.save()
def create_authdb_indexes(): db = cloudant_client[CL_AUTHDB] ddoc_fn = ''' function(doc){ if (doc.email) { emit(doc.email); } } ''' view_name = 'authdb-email-index' ddoc = DesignDocument(db, view_name) if ddoc.exists(): ddoc.fetch() ddoc.update_view(view_name, ddoc_fn) print('updated', view_name) else: ddoc.add_view(view_name, ddoc_fn) print('created', view_name) ddoc.save()
def test_view_callable_with_invalid_javascript(self): """ Test error condition when Javascript errors exist. This test is only valid for CouchDB because the map function Javascript is validated on the Cloudant server when attempting to save a design document so invalid Javascript is not possible there. """ self.populate_db_with_documents() ddoc = DesignDocument(self.db, 'ddoc001') ddoc.add_view('view001', 'This is not valid Javascript') ddoc.save() # Verify that the ddoc and view were saved remotely # along with the invalid Javascript del ddoc ddoc = DesignDocument(self.db, 'ddoc001') ddoc.fetch() view = ddoc.get_view('view001') self.assertEqual(view.map, 'This is not valid Javascript') try: for row in view.result: self.fail('Above statement should raise an Exception') except requests.HTTPError as err: self.assertEqual(err.response.status_code, 500)
def test_view_callable_with_invalid_javascript(self): """ Test error condition when Javascript errors exist """ self.populate_db_with_documents() ddoc = DesignDocument(self.db, 'ddoc001') ddoc.add_view( 'view001', 'This is not valid Javascript' ) ddoc.save() # Verify that the ddoc and view were saved remotely # along with the invalid Javascript del ddoc ddoc = DesignDocument(self.db, 'ddoc001') ddoc.fetch() view = ddoc.get_view('view001') self.assertEqual(view.map, 'This is not valid Javascript') try: for row in view.result: self.fail('Above statement should raise an Exception') except requests.HTTPError, err: self.assertEqual(err.response.status_code, 500)
def test_query_view_save_succeeds(self): """ Tests that save succeeds when language is query and views are query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { "_id": "_design/ddoc001", "language": "query", "views": { "view001": { "map": {"fields": {"name": "asc", "age": "asc"}}, "reduce": "_count", "options": {"def": {"fields": ["name", "age"]}, "w": 2}, } }, } self.db.create_document(data) ddoc = DesignDocument(self.db, "_design/ddoc001") ddoc.fetch() self.assertTrue(ddoc["_rev"].startswith("1-")) ddoc.save() self.assertTrue(ddoc["_rev"].startswith("2-"))
def test_cq_view_save_succeeds(self): """ Tests that save succeeds when language is query and views are query index views. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { '_id': '_design/ddoc001', 'language': 'query', 'views': { 'view001': {'map': {'fields': {'name': 'asc', 'age': 'asc'}}, 'reduce': '_count', 'options': {'def': {'fields': ['name', 'age']}, 'w': 2} } } } self.db.create_document(data) ddoc = DesignDocument(self.db, '_design/ddoc001') ddoc.fetch() self.assertTrue(ddoc['_rev'].startswith('1-')) ddoc.save() self.assertTrue(ddoc['_rev'].startswith('2-'))
def test_delete_a_query_index_view(self): """ Test deleting a query index view fails as expected. """ # This is not the preferred way of dealing with query index # views but it works best for this test. data = { "_id": "_design/ddoc001", "language": "query", "views": { "view001": { "map": {"fields": {"name": "asc", "age": "asc"}}, "reduce": "_count", "options": {"def": {"fields": ["name", "age"]}, "w": 2}, } }, } self.db.create_document(data) ddoc = DesignDocument(self.db, "_design/ddoc001") ddoc.fetch() with self.assertRaises(CloudantException) as cm: ddoc.delete_view("view001") err = cm.exception self.assertEqual(str(err), "Cannot delete a query index view using this method.")
class Portfolio: def __init__(self, stockAPIkey): print("Portfolio.init({0})".format(stockAPIkey)) self.stocks = dict( ) # Contains all stocks, both owned and tracked, with all their metadata # { symbol: {metadata} } self.categories = dict( ) # Contains major categories with arrays of (sub)categories and their metadata # { category: { target: %, actual: %, type: 'stock/fixed income' }} self.prices_last_updated = None # Might use this to update prices on a schedule self.foliodoc = None # placeholder for Cloudant document that stores portfolio metadata self.stockAPIkey = stockAPIkey self.barchartAPIkey = getenv('BARCHART') self.total_portfolio_value = 0 self.status = "" def load(self, db, portfolioname): print("Portfolio.load()") # Initialize database and variables self.name = portfolioname self.db = db # Initialize Cloudant database views self.stockddoc = DesignDocument(self.db, 'stocks') self.stockddoc.fetch() self.bycategory_view = self.stockddoc.get_view('bycategory') self.owned_view = self.stockddoc.get_view('owned') self.allowned_view = self.stockddoc.get_view('allowned') self.manualquote_view = self.stockddoc.get_view('manualquotes') self.folio_ddoc = DesignDocument(self.db, 'activefolios') self.folio_ddoc.fetch() self.active_folios_view = self.folio_ddoc.get_view('currentfolio') self.populate_categories() self.populate_stocks() self.refresh_total_value() # Load available category metadata into memory from DB def populate_categories(self): pass print("Portfolio.populate_categories()") # Load portfolio specification document from DB self.foliodoc = Document(self.db, self.name) self.foliodoc.fetch() for category in self.foliodoc['categories']: # print category for subcategory in self.foliodoc['categories'][category].keys(): # print "Subcategory name: {0} Target: {1}".format(subcategory, self.foliodoc['categories'][category][subcategory]) self.categories[subcategory] = dict( target=self.foliodoc['categories'][category][subcategory], actual=0, type=category) # Populate stock metadata in memory from DB. (tracked and owned) def populate_stocks(self): print("Portfolio.populate_stocks()") # get metadata on stocks we're tracking in the portfolio with self.bycategory_view.custom_result(include_docs=True, reduce=False) as rslt: for line in rslt: doc = line['doc'] if doc['symbol'] == 'Cash': temp_price = 1 else: temp_price = -1 self.stocks[doc['symbol']] = dict(symbol=doc['symbol'], name=doc['name'], comments=doc['comments'], active=doc['active'], buybelow=doc['buybelow'], lastprice=temp_price, category=doc['category'], qty=0) # Refresh current total value of portfolio for percentage calcuations and update subcategory totals def refresh_total_value(self): print("Portfolio.refresh_total_value()") self.total_portfolio_value = 0 # Make sure prices are current quote_success = self.refresh_all_stock_prices() if not quote_success: # Alert user that stock prices are potentially out of date self.status = 'WARNING: Stock price quote error. Data may be out of date.' # Update quantities of stocks we own with self.allowned_view.custom_result( reduce=True, include_docs=False, group_level=1) as resultcollection: for stock in resultcollection: self.stocks[stock['key']]['qty'] = stock['value'] # total up the account's value for stock in self.stocks.values(): self.total_portfolio_value = self.total_portfolio_value + ( stock['qty'] * stock['lastprice']) # Update each subcategory's percentage by summing the stocks within it # Right now this is a nested for loop, but through data in memory. category_value = 0 for category_name in self.categories.keys(): for stock in self.stocks.values(): if stock['category'] == category_name: category_value = category_value + stock[ 'lastprice'] * stock['qty'] self.categories[category_name]['actual'] = ( category_value / self.total_portfolio_value) * 100 category_value = 0 def get_subcategory_list(self): print("Portfolio.get_subcategory_list()") return self.categories.keys() def barchart_quote(self, symbols_string): # execute stock API call (BarChart) start_time = time() myurl = "https://marketdata.websol.barchart.com/getQuote.json" payload = { 'apikey': self.barchartAPIkey, 'only': 'symbol,name,lastPrice', 'symbols': symbols_string, 'mode': 'R' } try: r = requests.get(url=myurl, params=payload) print r.text data = r.json() end_time = time() print("Barchart API query time: {0} seconds".format( float(end_time - start_time))) return data except Exception as e: print "Cannot get quotes from BarChart: {0}".format(e) return None def alphavantage_quote(self, symbols_string): start_time = time() myurl = "https://www.alphavantage.co/query" payload = { 'function': 'BATCH_STOCK_QUOTES', 'symbols': symbols_string, 'apikey': self.stockAPIkey } try: r = requests.get(url=myurl, params=payload) data = r.json() end_time = time() print("AlphaVantage API query time: {0} seconds".format( float(end_time - start_time))) return data except Exception as e: print "Cannot get quotes from AlphaVantage: {0}".format(e) return None def refresh_all_stock_prices(self): print("Portfolio.refresh_all_stock_prices()") # Iterate through manual stock quotes in DB first (to cover missing symbols in stock APIs) with self.manualquote_view.custom_result(reduce=False) as rslt: manual_quotes = rslt[:] for row in manual_quotes: symbol = row['key'][0] date = row['key'][1] self.stocks[symbol]['lastprice'] = row['value'] # construct string for API call symbols_string = '' for symbol in self.stocks.keys(): if symbol <> "Cash": symbols_string = symbols_string + "{0},".format(symbol) # trim last comma symbols_string = symbols_string[:-1] ## execute stock API call (BarChart) barchartdata = self.barchart_quote(symbols_string) if barchartdata <> None: pass # Execute stock API call (AlphaVantage) alphavantagedata = self.alphavantage_quote(symbols_string) if alphavantagedata <> None: for stock_data in alphavantagedata['Stock Quotes']: # set the price of the holding in question if float(stock_data['2. price']) <> 0.0: self.stocks[stock_data['1. symbol']]['lastprice'] = float( stock_data['2. price']) # Update last quoted time self.prices_last_updated = int(time()) # Check for any missing stock prices (any that are not set will be -1) for stock in self.stocks.values(): if stock['lastprice'] == -1.0: return False return True # Create a new doc to track this stock and add it to the dictionary of stock data # custom ID is OK, since it prevents duplicate stock trackers # We should keep all prices in memory exclusively, or create "quote" docs # This DOES NOT store information about how much we own, because that's event sourced by transactions def new_stock(self, category, symbol, name, buybelow, comments, active): print("Portfolio.new_stock()") with Document(self.db, symbol) as stockdoc: stockdoc['type'] = 'stock' stockdoc['updated'] = strftime("%Y-%m-%d %H:%M:%S") stockdoc['category'] = category stockdoc['symbol'] = symbol stockdoc['active'] = active stockdoc['name'] = name stockdoc['comments'] = comments stockdoc['buybelow'] = buybelow self.stocks[symbol] = json.loads(stockdoc.json()) # Get a quote for the new stock self.stocks[symbol]['lastprice'] = -1 self.stocks[symbol]['qty'] = 0 # update all holdings and totals self.refresh_total_value() def new_transaction_doc(self, symbol, quantity, price, fee, action): xactiondoc = Document(self.db) xactiondoc['type'] = 'transaction' xactiondoc['action'] = action xactiondoc['quantity'] = quantity xactiondoc['date'] = strftime("%Y-%m-%d %H:%M:%S") xactiondoc['fee'] = fee xactiondoc['price'] = price if action == 'deposit' or action == 'withdrawl': xactiondoc['symbol'] = 'Cash' xactiondoc['price'] = 1 else: #otherwise use symbol passed and check to see if updating cash is needed xactiondoc['symbol'] = symbol xactiondoc.save() # Execute a transaction document and update cash balance (if appropriate) def trade(self, symbol, quantity, price, fee, action, usebalance): print("Portfolio.trade()") self.new_transaction_doc(symbol, quantity, price, fee, action) if (usebalance == True and symbol <> "Cash"): cashqty = (quantity * price) + fee if action == 'buy': cashaction = 'withdrawl' else: cashaction = 'deposit' self.new_transaction_doc('Cash', cashqty, 1, 0, cashaction) self.refresh_total_value() # Return currently cached metadata for this stock def get_stock(self, symbol): return self.stocks[symbol] # Get an individual stock symbol's quote via the API def get_quote(self, symbol): print("Portflio.get_quote({0})".format(symbol)) myurl = "https://www.alphavantage.co/query?function=BATCH_STOCK_QUOTES&symbols={0}&apikey={1}".format( symbol, self.stockAPIkey) try: r = requests.get(url=myurl) data = r.json() return float(data['Stock Quotes'][0]['2. price']) except Exception as e: print_local("Unable to get stock price: {0}".format(e)) return -1 # Update a tracked stock's metadata def update_stock(self, symbol, name, buybelow, comments, active): with Document(self.db, symbol) as doc: doc['updated'] = strftime("%Y-%m-%d %H:%M:%S") doc['name'] = str(name) if active == 'true': doc['active'] = True else: doc['active'] = False doc['buybelow'] = float(buybelow) doc['comments'] = str(comments) for x in ('updated', 'name', 'active', 'buybelow', 'comments'): self.stocks[symbol][x] = doc[x] # Neutralize content upon logout def clear(self): print("Portfolio.clear()") self.name = None self.db = None self.categories = dict() self.stocks = dict() self.prices_last_updated = None self.total_portfolio_value = 0 self.foliodoc = None # Return list of historical trasactions from DB def get_transactions(self, page, pagesize): print("Portfolio.get_transactions()") skip = page * pagesize ddoc = DesignDocument(self.db, 'transactions') ddoc.fetch() view = View(ddoc, 'history') return view(include_docs=False, limit=pagesize, skip=skip, reduce=False)['rows'] # Return a full state of the portfolio with metadata formatted for the Jinja template's rendering def get_template_data(self): print "Portfolio.get_template_data()" template_data = dict() # Iterate through the sub-categories for subcategory in self.categories.keys(): # print "Processing {0}:\nData: {1}".format(subcategory,self.categories[subcategory]) # local dictionary for this subcategory's data to go into the array above. Insert what we have so far subcategory_data = dict( type=subcategory, target_percentage=self.categories[subcategory]['target'], value= 0, # Tracks total value of all invested holdings in this particular subcategory (not used right now) actual_percentage="{0:,.1f}".format( self.categories[subcategory]['actual']), holdings=[] # array for all stocks in this subcat ) template_data[subcategory] = subcategory_data # print template_data # Iterate through all tracked stocks in this subcategory for stock in self.stocks.keys(): # local dictionary for this stock's data stock_data = dict( symbol=self.stocks[stock]['symbol'], name=self.stocks[stock]['name'], qty=self.stocks[stock]['qty'], price="$ {0:,.2f}".format(self.stocks[stock]['lastprice']), buy_below="$ {0:,.2f}".format(self.stocks[stock]['buybelow']), comments=self.stocks[stock]['comments'], value="$ {0:,.2f}".format( float(self.stocks[stock]['qty'] * self.stocks[stock]['lastprice']) ) # value of this security owned ) template_data[self.stocks[stock]['category']]['holdings'].append( stock_data) return template_data