{ "info": { "author": "Ian Ogilvy", "author_email": "support@salect.nz", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities" ], "description": ".. ViewModels documentation master README file.\n\n==========\nViewModels\n==========\n\n`Uses`_.\n--------\n\nViewModels is a wrapper for the data \u2018model\u2019, that include details of the data used in generating views. An ORM\n(https://en.wikipedia.org/wiki/Object-relational_mapping). The current implementation is with MongoDB for the bottle framework. Generally, the concept is to allow flexibility independent of the constraints of the underlying DB.\nViewModels provide for the model and also support the view code, so simplifies both model and view code.\n\n| -- `Interface to Provide Access to Database and Abstraction`_.\n| -- `Repository for All Information Relating to Data: Schema and Beyond`_.\n| -- `Increasing Range of Types Available to Applications`_.\n| -- `An Explanation of ViewModel Uses`_.\n\n`Background`_.\n--------------\n\n| -- `History`_.\n| -- `Data Tables/Collections and Data Views`_.\n\n`Instructions`_.\n----------------\n\n| -- `Simple Example`_.\n| -- `Describing a Table/Collection With ViewFields`_.\n| -- `Using 'ViewField' Derived Classes`_.\n| -- `'ViewModel' Interface`_.\n| -- `'ViewRow': The Row Interface`_.\n| -- `Extended ViewModel Declarations and Instancing`_.\n| -- `models_ and _sources`_.\n| -- `Setting Field Source`_.\n| -- `'ViewField' Interface`_.\n| -- `Building HTML Forms`_.\n| -- `Updating from HTML Forms`_.\n| -- `How to Load Test DB Data From JSON Files for Testing`_.\n\n`Data Relationships and Joins`_.\n--------------------------------\n\n| -- `Data Relationship Types`_.\n| -- `Joins`_.\n| -- `Inserts With Joins`_.\n\n`How It Works`_.\n----------------\n\n| -- `The Rows Structure`_.\n\nUses\n----\n\nInterface to Provide Access to Database and Abstraction\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nTo access a collection in a simply Mongo through pymongo could not be much more straightforward. Similarly with others.\nHowever, this does not provide:\n\n| -- abstraction between code and database;\n| -- types beyond those covered in the BSON typeset;\n| -- joins, and joins with 'lazy' execution;\n| -- a record of the schema in use;\n| -- support for a web maintenance interface to the database;\n| -- web interface supports security and templates for full application.\n\nAll these advantages are provided by using ViewModel. However, there are times\nwhen none of these justifies an additional layer. The more complex the\ncollection, the higher the amount of code, generally the higher the value\nof using ViewModels.\n\nAbstraction Between Code and Database\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDatabases migrate. The Salt database started with direct SQL, then\nSQLAlchemy, then MongoDB. Abstraction assists with migrations as the\ncode is written to abstract API, leaving the application able to remain\nunchanged during migration, and only internet interface to the new system\nneed change. In reality, some changes also require a change of API, but even\nin those cases, application changes are reduced.\nThe current Salt system uses Mongo and directly using the pymongo interface\nis can be perfect for simple access. A rewrite would be needed to change\nthem, but the code is so small it is not a significant barrier for small, uncomplicated\ncases. However, more complicated cases are another matter!\n\nRepository for *All* Information Relating to Data: Schema and Beyond\n++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nA single repository for all information about data. Information on both storage\nas well as information used for display, all in one place.\n\nData descriptions can be simple tables/collections or views which comprise multiple\ntables which are effectively joined.\n\nThe data description provided by ViewModel library can include extended types\ndescribed at a layer of abstraction separate from the storage specification,\nallowing the application layer free of the mechanics.\n\nViewModel was created for SQL based applications, but then evolved to also\nwork with NoSQL MongoDB applications.\n\nNoSql collections (or tables) can effectively be irregular with different\nfields present potentially in every entry. While with SQL, just examining a\nrow can give a reasonable view of that schema, but this can be less clear\nfrom NoSql. Even with SQL, the schema recorded is restricted to what the database\nengine requires, and lacks richer descriptions of the data and rules not\nimplemented by the database, but a repository for a schema becomes even more\nessential with NoSQL.\n\nIncreasing Range of Types Available to Applications\n+++++++++++++++++++++++++++++++++++++++++++++++++++\n\nViewModel provides a mapping between the data in the database and the data\nseen by the application. Far more descriptive types and more complex types\ncan be used by the application with the mapping between these types and\nthe underlying storage format handled by the ViewModel.\n\nAn Explanation of ViewModel Uses\n++++++++++++++++++++++++++++++++\n\nEvery window has a view even if it is just a view of a brick wall. In the case of ViewModel, each view has a window into the database at initialisation. Each window consists of an arbitrary number of rows. You can send the whole window, i.e. contents and attributes to the HTML browser in JSON format. The rules for how this JSON is shown in the browser is typically defined in the view.\n\nBackground\n----------\n\n| -- `History`_.\n| -- `Data Tables/Collections and Data Views`_.\n\nHistory\n+++++++\n\nThe original Salt project development worked with SQL at a time when\nthe SQLAlchemy project was still in early stages. So Salt developed its layer to abstract to the database in 2007 around the same time as SQLAlchemy\nwas developed. Both the salt 'DataModel' and SQLAlchemy libraries developed\nspecific advantages, but as a popular open sourced project, SQLAlchemy became\nthe more mature product.\nIn 2015 the Salt project chose to replace the internal 'DataModel' library\nwith the SQLAlchemy, due to wider use and greater development of the open\nsource project, but then found several key features of 'DataModel' were missing\nfrom SQLAlchemy.\nThe solution was a new library 'ViewModel', which acted as an abstraction layer between SQLAlchemy and the application. The name 'ViewModel' came from\nthe fact that the main elements present in 'DataModel' that were missing\nfrom SQLAlchemy were data extended data schema information that was also\nuseful in providing data description to views.\n\nThe next step brought the current 'ViewModel', by transforming that library to\nbecome an interface between pymongo and the application.\n\nData Tables/Collections and Data Views\n++++++++++++++++++++++++++++++++++++++\n\nThe ViewModel package focuses on preparing data for views. How is the data in a table/collection to be viewed? For example,\nconsider a 'Products' table or collection, where products may be viewed:\n\n| -- individually by product code;\n| -- as a list of products by product group, or by brand;\n| -- as a list through a custom search.\n\nThese become the views of the data from the database. It is never relevant to retrieve the entire table/collection for the products as if processing the entire table; each document will be processed in sequence.\nIn contrast, there may be other table/collections with either a single or\nsmall fixed number of rows/collections the entire table/collection may constitute\na view.\n\nFurther, the product table could have a join to a 'pack sizes' table/collection and\nfor some views, these are also part of the view.\n\nThe main concept is that each table has a set of relevant views of the\ntable/collection for various uses. The ViewModel specifies not just the\nschema of the table/collection, but the actual views of the table/collection.\n\n\nInstructions\n------------\n\n| -- `Simple Example`_.\n| -- `Describing a Table/Collection With ViewFields`_.\n| -- `Using 'ViewField' Derived Classes`_.\n| -- `Extended ViewModel Declarations and Instancing`_.\n| -- `Building HTML Forms`_.\n| -- `Updating from HTML Forms`_.\n\nSimple Example\n++++++++++++++\n\nThis example is given in advance the instructions or details on how the components of the example work. The idea is: read the example to gain an\noverview, then see more details to understand more and return to this\nexample.\n\nThe Simple Database\n~~~~~~~~~~~~~~~~~~~\nThe consider a database with a table of students. Rows or Documents have:\n\n| -- an id;\n| -- a name;\n| -- a course;\n| -- year number within the course.\n\nCode to Describe Table Find an Entry\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe code follows::\n\n from ViewModel import ViewModel, IdField, TxtField, IntField\n import pymongo\n\n database = pymongo.MongoClient(dbserver).get_database(\"example\")\n\n class StudentView(ViewModel):\n viewName_ = \"Students\"\n # models_ = #.Students\n id = IdField()\n name = TxtField()\n course = IntField()\n # .... field definitions may continue\n\n student = StudentView({}, models=database.Students)\n # could have used 'models_' within class to avoid needing 'models' parameter\n # for the init\n # {} empty dictionary to ensure an empty view, not needed if the database\n # does not even exist yet, as with a new database, initial view will always\n # be an empty view\n\n if len(student) > 0:\n print(\"oh no, we already have data somehow!\")\n\n students.insert_() # add an empty entry to our view\n\n with student: # use 'with', so changes written at the end of 'with'\n student.name = 'Fred'\n\n # ok ... now we have a 'Student' table with one entry\n\nCode to Read and Update Our Entry\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nA key concept is that while the class for the view describes a table, set of\ntables or joined tables (or collections in Mongo speak), an instance of\na ViewModel is the set of data or a window of the tables.\nInstancing the view reads from the database in most straightforward cases, although in more complicated cases the data may be read\nfrom the database when accessed, the view instance logically includes all data\nfrom a 'read' operation::\n\n # same class definition and imports as above\n\n student = StudentView({'name': 'Fred'},model = database.Students)\n # would save if we could have 'models_' in class definition!\n\n if not student.course:\n with student:\n student.course_year = 2\n student.course = 'Computing'\n\nMultiple Entry Views\n~~~~~~~~~~~~~~~~~~~~\n\nSo far our view has only one entry. An instance of our view is a window viewing\npart of the database. This window can be a single row/collection or a logical\ngroup of entries(from rows/collections), and for small tables, may even be\nthe entire table/collection. The code that follows adds another entry, so the sample has\nmore than one entry, then works with a multi-entry\nview::\n\n StudentView.models_ = database.Students\n # modify class, add 'models_' as an attribute,\n # this saves specifying 'models_' each time instancing StudentView\n\n student = StudentView()\n # no dictionary, this gives an empty view (not multi entry yet)\n\n student.insert_()\n with student: # adding a second student\n student.name = 'Jane'\n student.course = \"Computing\"\n student.course_year = 2\n\n # now our multi entry view for all year 2 Students\n students = StudentView({'course_year':2})\n\n for student in students:\n print(student.name)\n\nNote how multi-entry view instances can be treated as lists. In fact, single\nentry views can also be treated as a list, however for convenience view\nproperties for single entry views also allow direct access as one entry. For\na single entry view 'student'::\n\n student.name == student[0].name\n\n\nExample Summary\n~~~~~~~~~~~~~~~\n\nThe example bypasses the power of ViewModels to show you a simple introduction.\nA fundamental concept is that classes describe a table (or collection or set/join\nof tables). An *instance* of a ViewModel is one set specific subset, a set of\ndata from a table (or set/join of multiple tables).\n\nDescribing a Table/Collection With ViewFields\n+++++++++++++++++++++++++++++++++++++++++++++\n\nWhen creating a class derived from a ViewModel, add class attributes\nwhich are 'ViewFields' for each field in the table or collection.\n\nThe example (`Simple Example`_. ) uses several types of view fields. However\neach 'ViewField' can contain information well beyond the type of data.\nAn alternative name, a short and long description, formatting and other display\ndefaults, value constraints and many other settings, as well as a\n'default value' set with the 'value=' init parameter. Note that when a new\nrow is inserted into a view, no fields are set to their default value,\nand instead all fields, even those with default values, remain 'unset'. However\n'unset' fields return their default value when accessed.\nThis means that if a ViewModel can have a new field (or even merely a new default value for an existing field) added after several rows are already in\nthe database. Existing records will behave automatically return the\n'default value' even though they were saved prior to the default being defined.\nThis makes ViewModels stable and safe for software updates which add new fields\nwithout the need to update the database itself.\n\nIn the example, only the 'value' attribute of the \"name\" ViewField is accessed.\n'student.name' does not access the ViewField, but instead returns \"value\"\nattribute of the \"name\" ViewField. To access the\nactual ViewField (or IntField, TextField etc) and have access to these other\nattributes use 'student[\"name\"]' thus::\n\n student.name == student[\"name\"].value\n\n\nUsing 'ViewField' Derived Classes\n+++++++++++++++++++++++++++++++++\n\nAll 'fields' are sub-classed from ViewField and represent individual data types.\nEach field contains the following properties:\n\n| -- `name`: set explicitly, or defaulting to the property name;\n| -- `label`: set explicitly but defaulting to the name;\n| -- `hint`: defaults to '' for display;\n| -- `value`: returns value when a field is an attribute of a row object.\n\n\n'ViewModel' Interface\n+++++++++++++++++++++\n\nThe 'ViewModel' provides a base class defines a database table/collection, and each instance of\na ViewModel. Note all system properties and methods start of end with underscore to\navoid name collision with database field names.\n\nViewModel Interface Methods\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n| -- `insert\\_()`\n| -- `labelsList\\_()`\n| -- `update\\_()`\n| -- ` for row in `\n| -- ` [row]`\n\nViewModel Interface Properties\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n| -- `viewName\\_`\n| -- `models\\_`\n| -- `dbModels\\_`\n\nViewModel Details\n~~~~~~~~~~~~~~~~~\n\nThe `insert_()` method adds an empty new row (ViewRow instance) to the current ViewModel instance. At\nthe next `update_()`, an actual database document/row will be created, provided\nsome values have been set in the new row.\n\nNote that a record is currently marked for insert if there is no '_id', and otherwise\nfor update. So if a record created by `insert\\_()` has an '_id' added, currently\nthis record will then allow changes by update, without reading the record first.\n\nThe `labelsList_()` method returns a list of the labels from the rows of the current\nViewModel instance. It computes the list of labels by, first, looking for the row_label attribute if that fails then it will search through all possible fields for anything called rowLabel and then set row_label to the corresponding value of rowLabel. If rowLabel is not declared as True in the view definition, the rowLabel will default to 'no labels'.\n\nThe `update_()` method is called automatically at end of a 'with '\nstatement (python keyword 'with'), or can be called directly, to update the\nactual database with values\nchanged by assignments through '. = statements'.\n\n`viewName\\_` is merely a title for the view for display purposes.\n\n`models\\_` is a list of the names of tables, or actual database tables objects\nused by the view\n\n`dbModels\\_` is a dictionary of database table objects used by the view, with\nthe model names as keys.\n\nNote: all 'ViewModel' instances with one row implements all of the ViewRow interfaces in addition to the methods and properties discussed. 'ViewModel'\ninstances with more than one row will raise errors if the 'ViewRow' interface as it is ambiguous which row/document to use.\n\n'ViewRow': The Row Interface\n++++++++++++++++++++++++++++\n\nViewRow objects and ViewModel objects both implement the 'ViewRow' interface.\n\nWhere a ViewModel contains one logical row, the operations can be performed on the ViewModel, which also supports this interface for single row instances.\n\nViewRow Interface Methods\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n| -- `: for field in `\n| -- `loop\\_(case=): for field in a `\n| -- `: []`\n| -- ` .field_name`\n\nViewRow Interface Properties\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n| -- `fields\\_`\n| -- `view\\_`\n| -- `label\\_`\n| -- `idx\\_`\n\nViewRow Details\n~~~~~~~~~~~~~~~\n\nThe statement: `for in :` provides for using a 'for loop' to iterate\nover the fields in a row of a viewfield.\n\nNote that this iteration can be for building a view, and as such the iteration\nallows for selecting which fields are included in the view.\nWhen fields are declared\n(see `'ViewField' Interface`_), they can set a 'case' where they are applicable\nfor views.\nFor example, this can be in a view, on an edit panel, or the field is for\ncalculation purposes and part of the model, but not revealed in a view.\n\nUsing `[]` (or indexing), retrieves the instance of the\nViewField named. For example::\n\n student['name'].value = 'Jane'\n print(student['name'].value)\n\n # is equivalent to\n student.name = 'Jane'\n print(student.name)\n # but the point of using indexing to access other field attributes\n assert student['name'].wide == 16 # check the name field is 16 characters wide\n\n\n\n`fields\\_` returns a 'ViewRow' is a logical entry in a ViewModel. Consider the example\n( `Simple Example`_. ). The line of code::\n\n student.name = 'Fred'\n\nIs using the ViewRow set attribute interface to set the 'value' of the 'name' field within the 'row' created by the `insert\\_()` method.\n\nIn this example, because the 'student' ViewModel has only one row, the 'name' field can be accessed directly in the ViewModel. However, if there were, for example, three students in the view, which 'name' is to be changed? As stated previously,\nViewModel objects support the ViewRow interface but report an error if there is more\nthen one row.\n\nThere are two main ways to access 'ViewRow' objects (apart from simple treating the\nViewModel as also a ViewRow, which only works for single row views). If our 'student'\nViewModel\ncontains three students, there will be a row for each student, and these 'rows'\ncould be accessed as::\n\n students = StudentView({})\n assert len(students) == 3 # check we have 3 students\n student_0 = students[0]\n student_2 = students[2]\n for student in students:\n \n\n>From the ViewModel, indexing or iterating can access the ViewRows.\n\nThis interface allows retrieving and setting data 'fields' or ViewField entries by name as object attributes. All internal attributes of ViewRow have either\na trailing underscore to avoid name collisions with field names of the database, or a leading underscore to indicate that these attributes should not be accessed\nexternally of the ViewRow or ViewModel.\n\nProvided database fields have no leading or trailing underscore, they will not\ncollide with the names of internal workings of these classes.\n\nExtended ViewModel Declarations and Instancing\n++++++++++++++++++++++++++++++++++++++++++++++\n\ngetRows\n~~~~~~~\n\nThe `\\_\\_init\\_\\_()` method calls `getRows\\_` which is designed for subclassing.\ngetRows\\_ can return either:\n\n1. An empty list (for an empty view);\n2. The raw data from a find (where all data is from a single source and in this case the 'source' parameter to the class is used to build `dbRows\\_` automatically;\n3. A list of dicts (for the rows, dict with one entry for each 'source', and that entry itself being a dictionary of the fields of that 'source'.\n\nPrevious versions of the library required (2) to be instead a list of ObjDicts.\nThis is no longer supported. The statement::\n\n # below statement no longer will produce functioning code\n # remove it\n result = [ObjDict(res) for res in result]\n\n... would convert the result of a find into a list of ObjDicts, where each ObjDict is a row. What is now required is such data is embedded in a 'source' dictionary. A\nreplacement for the above line, (which is not need as the standard class init\nmethod will make\nthis adjustment automatically), would be the line::\n\n result = [Objdict(((row,res),)) for res in self.dbRows_]\n\n\n`models\\_` and `_sources`\n+++++++++++++++++++++++++\n\nAs the names suggest, 'models' is for 'public' use (or in this case declaration)\nand `_sources` is 'private'. The data to construct `_sources` is provided in\nbut the _sources class variable, or the 'sources' parameter to a viewmodel\nconstructor.\n\nIf sources (either `_sources` class variable or sources parameter), is not a list\nthen internal logic treats it as a one element list: [sources], so even if only\none value is provided, consider that value a one element list.\n\nEach value in the 'models' list can be one of the legacy values of 'None' or a MongoDB collection,\nor (preferred) an object instanced using a class based on the DBSource class. Currently,\nfour such classes exist: DBNoSource; DBVMSource; DBMongoSource and DBMongoEmbedSource.\n\nDBNoSource\n~~~~~~~~~~\n\nWhen generating a sources list from 'models', a value of None is used as a legacy alternative to creating a DBNoSource object, but the preferred way is an explicit object.\nFields with a 'NoSource', as the class name suggests, have no database source and thus no storage and as such are temporary values only. Since a collection or table name is not part\nof a 'NoSource' object, the source name must be described explicitly or will be '__None__'.\nNote that at the time of writing, any string entry in a source list that beginning with an\nunderscore will be taken as a DBNoSource object with the name of that string.\n\nDBVMSource\n~~~~~~~~~~\n\nA DBVMSource is used for data that exists within another ViewModel. This allows nested views.\nThis time, this is merely a provision for the future.\n\nDBMongoSource\n~~~~~~~~~~~~~\n\nThe source used for mongo collections, and instanced from legacy MongoDB collections, as\nwell as from the preferred explicit instances. The 'name' of a DBMongoSource is the name of the collection. So the collection 'students' would have the string name 'students'.\n\nDBMongoEmbedSource\n~~~~~~~~~~~~~~~~~~\nThese are used when the table is embedded within a document inside a mongo collection.\nThe source is specified as \".\", where the object list name is the object containing the entire embedded collection as a list of objects.\n\nDeclaring 'models\\_'\n~~~~~~~~~~~~~~~~~~~~\n\nModels (`models\\_`) may be declared as a class variable, or passed as a parameter ('models') to the\n`\\__init\\__()` method for the ViewModel.\n\nIn either case, the value is a list of each source, with each entry of one of the 'DBSource' types\nlisted above, or an application specific class derived from DBSource. Note that while models are in\ntheory a list, the code will convert a single entry into a list, eliminating the need to\nhave a single entry as a list.\n\n\nSetting Field Source\n++++++++++++++++++++\n\nAny field can belong to any 'source', as described above. The first 'source' for a view\nis considered the default source, so if using the first source, or 'default source', it is\npossible to omit the 'src=' parameter. Any field which is from a view other than the first view needs to specify the view by name with the 'src' parameter::\n\n src=\n\nFor an embedded source, the name will use 'dot notation'.\n\n\nFurther, a field may be embedded in another object. The name of the object should also be a\nspecified through source. Examples::\n\n models_ = DBMongoSource('students'), DBMongoSource('courses')\n\n num1 = IntField() # no 'src' specified -- field is in default 'students' collection\n num2 = IntField(src='courses') # field is in 'courses' table/collection\n num3 = IntField(src='courses.scores') # field is in scores object in courses table\n num4 = IntField(src='students.scores') # field is in scores object in students table\n num5 = IntField(src='.scores') # alternative using default notation, same location as 'num4'\n\n\n'ViewField' Interface\n+++++++++++++++++++++\n\nGetting and Setting 'Row Member' Values\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo be added\n\n\nBuilding HTML Forms\n+++++++++++++++++++\n\nTo be added\n\n\nUpdating from HTML Forms\n++++++++++++++++++++++++\n\nTo be added\n\n\nHow to Load Test DB Data From JSON Files for Testing\n++++++++++++++++++++++++++++++++++++++++++++++++++++\n\nLoading tables (collections) for testing is made easier by using the JSONLoad class provided in ViewModel. The class allows you to load previously downloaded JSON tables (Mongo collections -- just make sure they are created as JSON array types -- see `How to Export Mongo Databases/Collections to JSON `_ for more about this). The JSONLoad class is in \"json_load.py\".\n\nThe JSONLoad class sets the following defaults:\n\n-- The default JSONLoad location is \"dumped_data\". It is located at the same level as the test file (test_file.py) that is using the JSONLoad class (see below):\n ::\n\n project_root/\n |-- ...\n |-- tests/\n |-- dumped_data/\n |-- test_file.py\n |-- ...\n\n To override the default location, import \"DEFAULT_DUMP_DATA_FOLDER_NAME\" and set it to what you want it to be.\n\n-- The default host name & port number is::\n\n host_name = localhost\n port = 271017\n\n-- The default DB name is '' by design and is a required parameter i.e. db_name defaults to '' so must be passed in when you use JSONLoad::\n\n JSONLoad(db_name=\"MY_TEST_DB_NAME\")\n\n\nTo load JSON data into a test DB of your choice, follow the instruction below. The best place is in your \"conftest.py\" file if you are using pytest.\n\nTo import and use JSONLoad and optionally, DEFAULT_DUMP_DATA_FOLDER_NAME, include the following import statement in your test script::\n\n from viewmodel.json_load import JSONLoad, DEFAULT_DUMP_DATA_FOLDER_NAME\n\n\nOptionally, override the DEFAULT_DUMP_DATA_FOLDER_NAME with another in your script::\n\n DEFAULT_DUMP_DATA_FOLDER_NAME = 'my_alternate_folder_name'\n\nProvide a test DB name (here in a separate variable called TEST_DB) and create a test fixture that uses JSONLoad to call the method ```restore_db_from_json```::\n\n TEST_DB = 'my_test_db_name'\n\n @pytest.fixture(scope='session', autouse=True)\n def restore_db_from_json():\n JSONLoad(db_name=TEST_DB).restore_db_from_json()\n\nThen be sure to connect to your test DB::\n\n res = ObjDict(dbname=TEST_DB, dbserver=None)\n viewModelDB.baseDB.connect(res)\n\nJSONLoad Method Signatures\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n::\n\n \\__init\\__(host_name: str = 'localhost', port_number: int = 27017, db_name: str = None)\n insert_one(collection_name: str = None, data: Dict = None)\n insert_many(collection_name: str = None, data: List = None)\n drop_db(db_name: str)\n drop_collection(collection_name: str)\n read_json_data_file(path_to_file: str, file_name: str)\n load_data(collection_name: str, path_to_file: str, file_name: str)\n get_default_dumped_data_path()\n load_all(json_data_path: str = None)\n restore_db_from_json()\n\n\nData Relationships and Joins\n----------------------------\n\nThe term 'relational database' comes from the concept that data contained\nin separate tables (or collections) is related.\n\n\nData Relationship Types\n+++++++++++++++++++++++\n\nMany-to-One\n~~~~~~~~~~~\n\nThese are classic 'dry'. Several records (or rows or documents) in a table\nwill use the same information. For example, an address with a city. Since\nthere are far more addresses than cities, when reading an address, obtaining all\nthe 'city' information (name, city code, state) from a separate city table will\nmean that information for each city is not repeated for each address with the same\ncity. From the perspective of the address, the relationship is 'one-to-one' because\nfor each address there is only one city. The 'many-to-one' is that many addresses\nmay reference each city.\n\nIf our view is based on a single address, then retrieving the 'join' of the information\nfor the address together with the information for the city still leaves a single 'row' in the resulting view.\n\nIn database design, to implement a 'many-to-one', each entry from the many tables,\ncontains a key to the city table. Read an address, the use the 'key to the city' to read data from the city table.\n\n\nOne-to-Many\n~~~~~~~~~~~\n\n>From a technical perspective, this is simply the same as 'many-to-one', but viewed\nfrom the opposite perspective. However, the devil is in the detail, and having the opposite perspective has implications that can mean the correct implementation\nis very different. Looking at the previous cities and addresses, the 'one-to-many' view from the city perspective is to consider all addresses with the city.\n\nIf our view is based on a single city, then retrieving the 'join' would result in rows for\neach address. So while the one-to-many is the many-to-one from the opposite perspective,\nthe view changes entirely and in nature depending on which perspective.\n\nIn database design, the cross-reference key is still the 'key to the city' within the\naddress table. Read the city key (as 'our city key'). Then using the key field find all addresses with\ntheir 'key to the city' value matching the key in 'our city key'.\n\n\nOne of Many Selector\n~~~~~~~~~~~~~~~~~~~~\n\nThis is a real-world application of the 'many-to-one' join, where the table of possible 'ones' effective represents one of a finite set of choices which may be chosen from a 'drop-down list box'.\nViewModel has a specific Field Type, the 'EnumForeignField'. Note that to display choices for editing the entire table of choices is required. There are no strict formulae as to\nwhen the number of choices or total data of the choices table is too large but generally\nthe system must have the capacity to consider having the entire table in memory acceptable.\n\n\nMany-to-Many\n~~~~~~~~~~~~\n\nConsider now database with not just addresses and cities, but also people. Each person\nmight have a relationship to several addresses. However, rather than this being a\n'one-to-many' relationships, like the Cities -> Addresses, where viewed from the other\nperspective, Addresses -> Cities, for each address, there would be only one city, this time for each address there may be multiple people.\n\nIn database design, this usually represents more of a challenge. If we start with people, we cannot\nlook for addresses with a 'person key' field that matches since our person, since each address will need to match potentially several (or many) people. The matching person cannot be\nstored as a single value in our table. With SQL and even sometimes with NoSQL, the solution is to have a separate table of relationships. If we read this table for all entries\nmatching our person we can find an entry for each relationship to an address for that person.\nThis solves the problem because we can have more relationships than we have either\npeople or addresses, so one entry per table will not work without a particular table that can\nhave an entry for each relationship.\n\nNoSQL like Mongo provides another alternative, which is keeping a list of relationships inside\none (or even both) of the tables. Since an entry in the table can be a list, we could keep\na list of addresses in the people table. Read a person, and we have a list of addresses.\nRead an address, and we can read all people with our address in their address list. The principle is still the same, but there is this implementation choice.\n\nRelationship Specific Data\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIn some cases, there can be data specific to a relationship. Consider the following people,\naddresses and then relationships::\n\n People: Bob, Tom, Alice\n Addresses: RedHouse, Office1, Office2, GreenHouse\n Relationships:\n Bob: RedHouse is 'home', Office1 is 'work'\n Alice: RedHouse is 'home' and 'office'\n Tom: GreenHouse is Home, RedHouse is 'work1' and Office2 'work2'\n\nThe relationships between the people can each have their labels, just as the relationships between people can. In fact, each relationship can have a\nlabel from each perspective. Consider people relationships where Bob could be\n'husband' to Alice, but the same relationship from the other perspective could\nbe 'wife'.\n\nSo for Bob, we may have to have not only added 'RedHouse' and created a relationship,\nwe also have to manage a label for the relationship.\n\nJoins\n+++++\n\nIn SQL, a join is a read, or update, of data from more than one table. The join uses\nthe relationship\nbetween tables to select rows of data that combine information from multiple tables.\nEach table in the join is effectively a source of data.\n\nViewModel support data from multiple sources, but currently this has only been used\nto support joins from relationship tables and tables that are part of the relationship.\n\n\nInserts With Joins\n++++++++++++++++++\n\nWhen a new document is inserted for any source within a ViewModel,\nfields within the current view can be automatically\nupdated to reference the new `_id` generated. These fields should be listed in\nthe `_sources[].join_links` list. This list is the field names\nto be updated.\n\n\nHow It Works\n------------\n\nThe Rows Structure\n++++++++++++++++++\n\nThe actual data is kept in a view list called `dbRows\\_`,\nwhich reflects the actual data being held in the underlying\ndatabase.\nFor each row of the view, there is one entry in `dbRows\\_`.\n\nThe List of Elements of 'dbRows\\_'\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nEach entry is of type 'objdict' and the elements of the\nobjdict were originally the values of the fields in the\nview, but a new layer has been added, so that 'objdict'\nentries at the top level represent the data from a single source.\n\nFrom::\n\n [ {'name':'Jane','course':'computing'}]\n\nTo::\n\n [ {'students': {'name':'Jane','course':'computing'}}]\n\nThe two-tiered structure, keyed by the 'table/collection'\nwhich is the data source, better provides for data from\nmultiple sources.\n\nData is not added directly to these rows but through the 'viewmodel_row' wrappers. So if a ViewModel row has a view_field (say 'last_name')\nwhich is not present in the row, setting the name would add\na new field to the appropriate ObjDict within the row, but also an\nan entry to an additional 'changes' copy of the row, which holds new values\nnot yet committed to the database.\n\nThe 'rows' and 'changes' are the bridges between what is in the\ndatabase files, and what is held in memory.\n\nThe DBSource Descriptor\n~~~~~~~~~~~~~~~~~~~~~~~\n\nSee the DBSource class documentation, but this class describes the sources of data\nthat are held within the dbRows.\n\nEach 'row' has a set of a least one 'source'. Source types can be MongoDB table,\nMongoDB document, memory, (and soon) another view.\n\nEach source requires a method to load from the source, and update to the source. 'getrows' methods\ncurrently takes a 'load filter' and uses that to load all sources, but\na structure is required to more flexible to handle all sources.\n\nUpdate methods again handle all source types.\n\nIt is suggested that a useful revision would be to have 'getrows' that calls a `src_getrows`\nfor each source and update call `src_update()` for each source.\n\nNew getRows\n~~~~~~~~~~~\n\nA new getrows would take a filter dictionary or list as valid parameters.\nEach entry would need a lead and a lazy. Run 'leads' in sequence until lead returns a non zero list.\nList is applied for that source, all other sources are empty, but have 'lazy' load available.\n\nOnce a lead returns true, the `scr_getrows_table()` would apply a dictionary;\n\n\n", "description_content_type": "", "docs_url": null, "download_url": "", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://bitbucket.org/objdict/viewmodel", "keywords": "database bottle mongodb pymongo view mvc model", "license": "LGPL", "maintainer": "", "maintainer_email": "", "name": "ViewModel", "package_url": "https://pypi.org/project/ViewModel/", "platform": "", "project_url": "https://pypi.org/project/ViewModel/", "project_urls": { "Homepage": "https://bitbucket.org/objdict/viewmodel" }, "release_url": "https://pypi.org/project/ViewModel/0.3.6/", "requires_dist": [ "bottle", "mako", "objdict", "pymongo" ], "requires_python": "", "summary": "Model and View support for bottle framework, currently supports MongoDB. The ViewModel provides a high level DB schema and interface to a database as well as an interface from the DB to views. Current version works with bottle framework and pymongo however a previous version supported SQLAlchemy and other frameworks could be supported.", "version": "0.3.6" }, "last_serial": 3915782, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "bae92032e765f6343f789c1f3322d3e1", "sha256": "09c0dbad6c4820d3f5889ca89d99e9a4192d69f4146e3c3567441717b45e4f99" }, "downloads": -1, "filename": "ViewModel-0.1.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "bae92032e765f6343f789c1f3322d3e1", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 22045, "upload_time": "2016-09-26T02:52:43", "url": "https://files.pythonhosted.org/packages/1e/bc/0aeb3eb69bf8f9e6c3b4a19b5c36f6a6bba4e515afe6c0c9b47ae5280f5d/ViewModel-0.1.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "3b4f0637d7d8b521f7bbaecb1b84e5ce", "sha256": "3e6378cc5596e2e52619b6b08c614ccd55e8d11a8ffc74da120f72c5a38e1812" }, "downloads": -1, "filename": "ViewModel-0.1.0.tar.gz", "has_sig": false, "md5_digest": "3b4f0637d7d8b521f7bbaecb1b84e5ce", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 21674, "upload_time": "2016-09-26T02:52:57", "url": "https://files.pythonhosted.org/packages/82/96/d08d4603533d427fd13ad1571684aca72bf6c19380e60648e73409f5e26b/ViewModel-0.1.0.tar.gz" } ], "0.2.0": [ { "comment_text": "", "digests": { "md5": "a35f3fcbaad60547c4177b08adc66082", "sha256": "3d4f9ea684a9e6540824c60f2b5f06f2906a478c1d93379e7f1ff7876e9392ec" }, "downloads": -1, "filename": "ViewModel-0.2.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "a35f3fcbaad60547c4177b08adc66082", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 38789, "upload_time": "2016-10-07T07:05:15", "url": "https://files.pythonhosted.org/packages/89/ea/dba41cd1d81f80106b3a94014f48fc6bb6ddf1fc5232250f55b6c561ee40/ViewModel-0.2.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "966195725986dc28c2934d9a766d6e80", "sha256": "6c7813dee2d25b6ce3a4ed6c292fb522756241332d7b83b49611373cb701e0d2" }, "downloads": -1, "filename": "ViewModel-0.2.0.tar.gz", "has_sig": false, "md5_digest": "966195725986dc28c2934d9a766d6e80", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 37098, "upload_time": "2016-10-07T07:05:28", "url": "https://files.pythonhosted.org/packages/a3/03/861a8656e4367852ea31ed01b962375adad3c88648c1a3c9df0a469441e6/ViewModel-0.2.0.tar.gz" } ], "0.2.1": [ { "comment_text": "", "digests": { "md5": "eb92d5a4f6e3b537b2c6a643d2e036bb", "sha256": "ea2475dc1876568c1327df1eae307ec28fbed3fff91702bf2fa38b3e1e9b7e91" }, "downloads": -1, "filename": "ViewModel-0.2.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "eb92d5a4f6e3b537b2c6a643d2e036bb", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 38359, "upload_time": "2016-10-07T07:29:27", "url": "https://files.pythonhosted.org/packages/c6/be/a6c354bac51d60408a2f0c8382863f96dca725294fdbe54c4b4f2e18bef3/ViewModel-0.2.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "cbf6f0c4329f40aec44027f604991460", "sha256": "0594c4b47688a400aa55f51dd8abbc6854358d68e62641e2fa0eb0ea17fa062e" }, "downloads": -1, "filename": "ViewModel-0.2.1.tar.gz", "has_sig": false, "md5_digest": "cbf6f0c4329f40aec44027f604991460", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 37164, "upload_time": "2016-10-07T07:29:43", "url": "https://files.pythonhosted.org/packages/e5/5a/5503ffb0ce733da0b63610bdfdffff97e1cfff3d7b492eca276ec1c67f97/ViewModel-0.2.1.tar.gz" } ], "0.3.0": [ { "comment_text": "", "digests": { "md5": "6bac54c6f0388f12760b0a99ed3654f7", "sha256": "e3a2be1604e14a109e2612bbd42902165acb3ff5863c1e76b0cac36d7e4bf751" }, "downloads": -1, "filename": "ViewModel-0.3.0-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "6bac54c6f0388f12760b0a99ed3654f7", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 45796, "upload_time": "2016-12-19T00:39:04", "url": "https://files.pythonhosted.org/packages/63/ab/d257bf5811d841eadddf396678533b445503949d1d248e053ffb7ec5b993/ViewModel-0.3.0-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "76555690c05a9a5e771fb78dcd4fbc1b", "sha256": "4c0ac0790bf34a8e8829baab0bff6347be08ccdbd1194225a2940969927704e5" }, "downloads": -1, "filename": "ViewModel-0.3.0.tar.gz", "has_sig": false, "md5_digest": "76555690c05a9a5e771fb78dcd4fbc1b", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 45070, "upload_time": "2016-12-19T00:38:50", "url": "https://files.pythonhosted.org/packages/b3/37/005e1df386e53f16dba0f39adda12550b76c44712e81d5bab03a5bef4b59/ViewModel-0.3.0.tar.gz" } ], "0.3.1": [ { "comment_text": "", "digests": { "md5": "a5c507de90da00d1f1a1dc2453db8e71", "sha256": "200e55ea9e663bdb4464e848ab84d71da0ed90d95d5f632a8945f9314034f8e7" }, "downloads": -1, "filename": "ViewModel-0.3.1-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "a5c507de90da00d1f1a1dc2453db8e71", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 45754, "upload_time": "2016-12-19T22:58:52", "url": "https://files.pythonhosted.org/packages/ee/a9/ac9b0c3a750c515df10f9095239b224ef0e9bacc9a46cd59728f6e7fe749/ViewModel-0.3.1-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "9a46e2a77dab260b263eebf1476c09d9", "sha256": "7d454cb2e49e0edf43436549369f87d4090a907de7a9281c2f9f4d6eb103ad6d" }, "downloads": -1, "filename": "ViewModel-0.3.1.tar.gz", "has_sig": false, "md5_digest": "9a46e2a77dab260b263eebf1476c09d9", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 45043, "upload_time": "2016-12-19T22:59:04", "url": "https://files.pythonhosted.org/packages/49/76/243e0b829052ba615a133db9338396a2b92f0d90c2458b5b3a190e8e889a/ViewModel-0.3.1.tar.gz" } ], "0.3.2": [ { "comment_text": "", "digests": { "md5": "e7e8316bec9273eeb3bf315d1af1241e", "sha256": "0573d6994990a3aa2c17453def104683782e0af2d37840cbdb549d4d0cb7ac5f" }, "downloads": -1, "filename": "ViewModel-0.3.2-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "e7e8316bec9273eeb3bf315d1af1241e", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 55019, "upload_time": "2017-02-21T13:41:58", "url": "https://files.pythonhosted.org/packages/70/39/fdafbd3493eb499790da57b7e2f5f3af4a3f26b356cafbfbe43ff3b944a7/ViewModel-0.3.2-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "d0f65a084e83d3f5dc66e9691e82e7e2", "sha256": "f5c9354fd330f75d7079e52136919f9e3e6b36f75ee4af07949edb7baa76b88b" }, "downloads": -1, "filename": "ViewModel-0.3.2.tar.gz", "has_sig": false, "md5_digest": "d0f65a084e83d3f5dc66e9691e82e7e2", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 61659, "upload_time": "2017-02-21T13:42:20", "url": "https://files.pythonhosted.org/packages/4d/7e/db12120c4abc3a35343d2549495275df9688f1e6f4660f51ec91fe15ff37/ViewModel-0.3.2.tar.gz" } ], "0.3.3": [ { "comment_text": "", "digests": { "md5": "7659a7ff915cff2a4bc59d1def266b8e", "sha256": "8eead247e0e7a470b1cbe2cb0d27e4fcdb4154877d883289aeed16915ba4d852" }, "downloads": -1, "filename": "ViewModel-0.3.3-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "7659a7ff915cff2a4bc59d1def266b8e", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 61946, "upload_time": "2017-06-25T07:19:20", "url": "https://files.pythonhosted.org/packages/ec/49/30fb6b4e7a52ade88eac06c2405fdb9ee118e48d1dcb8a4997d647949b98/ViewModel-0.3.3-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "c98a83a0db1019f8ad4e0ac22be75061", "sha256": "d98db87a0ffa140e2273ae55766c1cf135288ade288b1da16f4b6edd95603a9e" }, "downloads": -1, "filename": "ViewModel-0.3.3.tar.gz", "has_sig": false, "md5_digest": "c98a83a0db1019f8ad4e0ac22be75061", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 65494, "upload_time": "2017-06-25T07:18:18", "url": "https://files.pythonhosted.org/packages/99/91/79aaf6377c0738df8aad4cab17360a23767f87de9ebbd1cb4a9b7a7607e1/ViewModel-0.3.3.tar.gz" } ], "0.3.4": [ { "comment_text": "", "digests": { "md5": "b95fdaf331490a88c197c06c0cb0bacb", "sha256": "b2128813e519ad11a09bdbe652562705825286724dae6532a8fa83374e78d952" }, "downloads": -1, "filename": "ViewModel-0.3.4-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "b95fdaf331490a88c197c06c0cb0bacb", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 67340, "upload_time": "2017-10-17T00:35:25", "url": "https://files.pythonhosted.org/packages/de/25/452f7ab73e87f9472b7c7726e230ea67ca4884ef31322fb0c1d70cc69cf3/ViewModel-0.3.4-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "0469b381295e800583801152c0ebed1f", "sha256": "549f3e8915e71eb5dc93c604e0e60789e212fe453e87767466f4226771f41a18" }, "downloads": -1, "filename": "ViewModel-0.3.4.tar.gz", "has_sig": false, "md5_digest": "0469b381295e800583801152c0ebed1f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 72619, "upload_time": "2017-10-17T00:35:14", "url": "https://files.pythonhosted.org/packages/19/8a/793c4a6c8d8f38661bb5100814c9659a476d99cdb50eea773deaa328eba8/ViewModel-0.3.4.tar.gz" } ], "0.3.5": [ { "comment_text": "", "digests": { "md5": "2f50ee8f6d73ff7d194f93f00451b2cf", "sha256": "53620dfe0ad102408950c8fe902ac32225f7282f2984bbb8b867f6570aab05ea" }, "downloads": -1, "filename": "ViewModel-0.3.5-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "2f50ee8f6d73ff7d194f93f00451b2cf", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 73191, "upload_time": "2018-05-29T10:26:26", "url": "https://files.pythonhosted.org/packages/2d/bf/2e57d8270b6a19f60112c11e1e36bf333eb65ec26c2b2a949c3a9c7c28b0/ViewModel-0.3.5-py2.py3-none-any.whl" }, { "comment_text": "", "digests": { "md5": "318d574e7cec7ae57f3f29c2536eb9f8", "sha256": "ddefb59b9dcb78b585fa04c73f12e14cb283a805bb2dbc4074b4a1d15e3ffa29" }, "downloads": -1, "filename": "ViewModel-0.3.5.tar.gz", "has_sig": false, "md5_digest": "318d574e7cec7ae57f3f29c2536eb9f8", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 77752, "upload_time": "2018-05-29T10:27:25", "url": "https://files.pythonhosted.org/packages/5d/d1/a0a275b0dcddcfc86f1e1bc414712daa154c7e544f0ae4d3453689d22c62/ViewModel-0.3.5.tar.gz" } ], "0.3.6": [ { "comment_text": "", "digests": { "md5": "c520fc61698dccacd7482c0130cb9ac7", "sha256": "fc9c6fdf6545c815f3df8faebff2878a4bd4204dcbe29dd8c324be854e98e0f1" }, "downloads": -1, "filename": "ViewModel-0.3.6-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "c520fc61698dccacd7482c0130cb9ac7", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 70186, "upload_time": "2018-05-31T04:58:52", "url": "https://files.pythonhosted.org/packages/12/96/6d0381513ff854a626e692e5b680f5db75bbc0153558d8e78f9dfbeadb56/ViewModel-0.3.6-py2.py3-none-any.whl" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "c520fc61698dccacd7482c0130cb9ac7", "sha256": "fc9c6fdf6545c815f3df8faebff2878a4bd4204dcbe29dd8c324be854e98e0f1" }, "downloads": -1, "filename": "ViewModel-0.3.6-py2.py3-none-any.whl", "has_sig": false, "md5_digest": "c520fc61698dccacd7482c0130cb9ac7", "packagetype": "bdist_wheel", "python_version": "py2.py3", "requires_python": null, "size": 70186, "upload_time": "2018-05-31T04:58:52", "url": "https://files.pythonhosted.org/packages/12/96/6d0381513ff854a626e692e5b680f5db75bbc0153558d8e78f9dfbeadb56/ViewModel-0.3.6-py2.py3-none-any.whl" } ] }