NeoSchema Reference Guide

This guide is for version Beta 29


2 classes: NeoSchema, SchemaCache (helper class not meant for end user)    Source code

Background Information: Using Schema in Graph Databases such as Neo4j

Tutorial



CONTENTS:

Class NeoSchema

Class SchemaCache


Class NeoSchema


    A layer above the class NeoAccess (or, in principle, another library providing a compatible interface)
    to provide an optional schema to the underlying database.

    Schemas may be used to either:
        1) acknowledge the existence of typical patterns in the data
        OR
        2) to enforce a mold for the data to conform to

    MOTIVATION

        Relational databases are suffocatingly strict for the real world.
        Neo4j by itself may be too anarchic.
        A schema (whether "lenient/lax/loose" or "strict") in conjunction with Neo4j may be the needed compromise.

    GOAL

        To infuse into Neo4j functionality that some people turn to RDF, or to relational databases, for.  
        However, carve out a new path rather than attempting to emulate RDF or relational databases!


    SECTIONS IN THIS CLASS:
        * CLASS-related
            - RELATIONSHIPS AMONG CLASSES
        * PROPERTIES-RELATED
        * SCHEMA-CODE  RELATED
        * DATA NODES
        * DATA IMPORT
        * EXPORT SCHEMA
        * INTERNAL  METHODS



    OVERVIEW

        - "Class" nodes capture the abstraction of entities that share similarities.
          Example: "car", "star", "protein", "patient"

          In RDFS lingo, a "Class" node is the counterpart of a resource (entity)
                whose "rdf:type" property has the value "rdfs:Class"

        - The "Property" nodes linked to a given "Class" node, represent the attributes of the data nodes of that class

        - Data nodes are linked to their respective classes by a "SCHEMA" relationship.

        - Some classes contain an attribute named "schema_code" that identifies the UI code to display/edit them,
          as well as their descendants under the "INSTANCE_OF" relationships.
          Conceptually, the "schema_code" is a relationship to an entity consisting of software code.

        - Class can be of the "S" (Strict) or "L" (Lenient) type.
            A "lenient" Class will accept data nodes with any properties, whether declared in the Class Schema or not;
            by contrast, a "strict" class will prevent data nodes that contains properties not declared in the Schema

            (TODO: also implement required properties and property data types)


    IMPLEMENTATION DETAILS

        - Every node used by this class has a unique attribute "schema_id",
          containing a non-negative integer.
          Similarly, data nodes have a separate unique attribute "item_id" (TODO: rename "uri" or "token")

        - The names of the Classes and Properties are stored in node attributes called "name".
          We also avoid calling them "label", as done in RDFS, because in Labeled Graph Databases
          like Neo4j, the term "label" has a very specific meaning, and is pervasively used.

        - For convenience, data nodes contain a redundant attribute named "schema_code"


    AUTHOR:
        Julian West


    TODO:   - continue the process of making the methods more efficient,
              by directly generate Cypher code, rather than using high-level methods in NeoAccess;
              for example, as done by create_data_node()
            - complete the switch-over from integer "item_id" to string "uri"


    ----------------------------------------------------------------------------------
	MIT License

        Copyright (c) 2021-2023 Julian A. West

        This file is part of the "Brain Annex" project (https://BrainAnnex.org)

        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:

        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.

        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
	----------------------------------------------------------------------------------
    
nameargumentsreturns
set_databasecls, dbNone
        IMPORTANT: this method MUST be called before using this class!

        :param db:  Database-interface object, to be used with the NeoAccess library
        :return:    None
        
nameargumentsreturns
assert_valid_class_namecls, class_name: strNone
        Raise an Exception if the passed argument is not a valid Class name

        :param class_name:
        :return:
        
nameargumentsreturns
create_classcls, name :str, code = None, strict = False, no_datanodes = False(int, int)
        Create a new Class node with the given name and type of schema,
        provided that the name isn't already in use for another Class.

        Return a pair with the Neo4j ID of the new ID,
        and the auto-incremented unique ID assigned to the new Class.
        Raise an Exception if a class by that name already exists.

        NOTE: if you want to add Properties at the same time that you create a new Class,
              use the function create_class_with_properties() instead.

        TODO: offer the option to link to an existing Class, like create_class_with_properties() does
                  link_to=None, link_name="INSTANCE_OF", link_dir="OUT"
        TODO: maybe an option to add multiple Classes of the same type at once???
        TODO: maybe stop returning the schema_id ?

        :param name:        Name to give to the new Class
        :param code:        Optional string indicative of the software handler for this Class and its subclasses
        :param strict:      If True, the Class will be of the "S" (Strict) type;
                                otherwise, it'll be of the "L" (Lenient) type
                            Explained under the comments for the NeoSchema class

        :param no_datanodes If True, it means that this Class does not allow data node to have a "SCHEMA" relationship to it;
                                typically used by Classes having an intermediate role in the context of other Classes

        :return:            A pair of integers with the Neo4j ID and the unique schema_id assigned to the node just created,
                                if it was created;
                                an Exception is raised if a class by that name already exists
        
nameargumentsreturns
get_class_internal_idcls, class_name: strint
        Returns the Neo4j ID of the Class node with the given name,
        or raise an Exception if not found, or if more than one is found.
        Note: unique Class names are assumed.

        :param class_name:  The name of the desired class
        :return:            The Neo4j ID of the specified Class
        
nameargumentsreturns
get_class_idcls, class_name: str, namespace=Noneint
        Returns the Schema ID of the Class with the given name, or -1 if not found
        TODO: unique Class names are assumed.  Perhaps add an optional "namespace" attribute, to use in case
              multiple classes with the same name must be used
        TODO: maybe raise an Exception if more than one is found

        :param class_name:  The name of the desired class
        :param namespace:   EXPERIMENTAL - not yet in use
        :return:            The Schema ID of the specified Class, or -1 if not found
        
nameargumentsreturns
get_class_id_by_neo_idcls, internal_class_id: intint
        Returns the Schema ID of the Class with the given internal database ID.

        :param internal_class_id:
        :return:            The Schema ID of the specified Class; raise an Exception if not found
        
nameargumentsreturns
class_neo_id_existscls, neo_id: intbool
        Return True if a Class by the given internal ID ID already exists, or False otherwise

        :param neo_id:
        :return:
        
nameargumentsreturns
class_id_existscls, schema_id: intbool
        Return True if a Class by the given schema ID already exists, or False otherwise

        :param schema_id:
        :return:
        
nameargumentsreturns
class_name_existscls, class_name: strbool
        Return True if a Class by the given name already exists, or False otherwise

        :param class_name:  The name of the class of interest
        :return:            True if the Class already exists, or False otherwise
        
nameargumentsreturns
get_class_namecls, schema_id: intstr
        Returns the name of the class with the given Schema ID, or "" if not found

        :param schema_id:   An integer with the unique ID of the desired class
        :return:            The name of the class with the given Schema ID, or "" if not found
        
nameargumentsreturns
get_class_name_by_neo_idcls, class_neo_id: intstr
        Returns the name of the class with the given Neo4j ID, or raise an Exception if not found

        :param class_neo_id:    An integer with the Neo4j ID of the desired class
        :return:                The name of the class with the given Schema ID;
                                    raise an Exception if not found
        
nameargumentsreturns
get_class_attributescls, class_internal_id: intdict
        Returns all the attributes (incl. the name) of the Class node with the given internal database ID,
        or raise an Exception if the Class is not found.
        If no "name" attribute is found, an Exception is raised.

        :param class_internal_id:   An integer with the Neo4j ID of the desired class
        :return:                    A dictionary of attributed of the class with the given Schema ID;
                                        an Exception is raised if not found
                                        EXAMPLE:  {'name': 'MY CLASS', 'schema_id': 123, 'type': 'L'}
        
nameargumentsreturns
get_all_classescls, only_names=True[str]
        Fetch and return a list of all the existing Schema classes - either just their names (sorted alphabetically)
        (TODO: or a fuller listing - not yet implemented)

        TODO: disregard capitalization in sorting

        :return:    A list of all the existing Class names
        
nameargumentsreturns
delete_classcls, name: str, safe_delete=TrueNone
        Delete the given Class AND all its attached Properties.
        If safe_delete is True (recommended) delete ONLY if there are no data nodes of that Class
        (i.e., linked to it by way of "SCHEMA" relationships.)

        :param name:        Name of the Class to delete
        :param safe_delete: Flag indicating whether the deletion is to be restricted to
                            situations where no data node would be left "orphaned".
                            CAUTION: if safe_delete is False,
                                     then data nodes may be left without a Schema
        :return:            None.  In case of no node deletion, an Exception is raised
        
nameargumentsreturns
is_strict_classcls, class_internal_id: int, schema_cache=Nonebool

        :param class_internal_id:   The internal ID of a Schema Class node
        :param schema_cache:        (OPTIONAL) "SchemaCache" object
        :return:                    True if the Class is "strict" or False if not (i.e., if it's "lax")
        
nameargumentsreturns
allows_data_nodescls, class_name = None, class_neo_id = None, schema_cache=Nonebool
        Determine if the given Class allows data nodes directly linked to it

        :param class_name:      Name of the Class
        :param class_neo_id :   (OPTIONAL) Alternate way to specify the class; if both specified, this one prevails
        :param schema_cache:    (OPTIONAL) "SchemaCache" object
        :return:                True if allowed, or False if not
                                    If the Class doesn't exist, raise an Exception
        

RELATIONSHIPS AMONG CLASSES

nameargumentsreturns
assert_valid_class_identifiercls, class_node: Union[int, str]None
        Raise an Exception is the argument is not a valid "identifier" for a Class node,
        meaning either a valid name or a valid internal database ID

        :param class_node:    Either an integer with the internal database ID of an existing Class node,
                                or a string with its name
        :return:
        
nameargumentsreturns
create_class_relationshipcls, from_class: Union[int, str], to_class: Union[int, str], rel_name="INSTANCE_OF"None
        Create a relationship (provided that it doesn't already exist) with the specified name
        between the 2 existing Class nodes (identified by their internal database ID's or name),
        in the ( from -> to ) direction.

        In case of error, an Exception is raised

        Note: multiple relationships by the same name between the same nodes are allowed by Neo4j,
              as long as the relationships differ in their attributes
              (but this method doesn't allow setting properties on the new relationship)

        TODO: add a method that reports on all existing relationships among Classes?
        TODO: allow properties on the relationship

        :param from_class:  Either an integer with the internal database ID of an existing Class node,
                                or a string with its name.
                                Used to identify the node from which the new relationship originates.
        :param to_class:    Either an integer with the internal database ID of an existing Class node,
                                or a string with its name.
                                Used to identify the node to which the new relationship terminates.
        :param rel_name:    Name of the relationship to create, in the from -> to direction
                                (blanks allowed)
        :return:            None
        
nameargumentsreturns
rename_class_relcls, from_class: int, to_class: int, new_rel_namebool
        #### TODO: NOT IN CURRENT USE
        Rename the old relationship between the specified classes
        TODO: if more than 1 relationship exists between the given Classes,
              then they will all be replaced??  TO FIX!  (the old name ought be provided)

        :param from_class:
        :param to_class:
        :param new_rel_name:
        :return:            True if another relationship was found, and successfully renamed;
                            otherwise, False
        
nameargumentsreturns
delete_class_relationshipcls, from_class: str, to_class: str, rel_nameint
        Delete the relationship(s) with the specified name
        between the 2 existing Class nodes (identified by their respective names),
        going in the from -> to direction direction.
        In case of error or if no relationship was found, an Exception is raised

        Note: there might be more than one - relationships with the same name between the same nodes
              are allowed, provided that they have different properties.
              If more than one is found, they will all be deleted.  (TODO: test)
              The number of relationships deleted will be returned

        :param from_class:  Name of one existing Class node (blanks allowed in name)
        :param to_class:    Name of another existing Class node (blanks allowed in name)
        :param rel_name:    Name of the relationship(s) to delete,
                                if found in the from -> to direction (blanks allowed in name)

        :return:            The number of relationships deleted.
                            In case of error, or if no relationship was found, an Exception is raised
        
nameargumentsreturns
unlink_classescls, class1: int, class2: intbool
        Remove ALL relationships (in any direction) between the specified classes

        :param class1:  Integer ID to identify the first Class
        :param class2:  Integer ID to identify the second Class
        :return:        True if exactly one relationship (in either direction) was found, and successfully removed;
                        otherwise, False
        
nameargumentsreturns
class_relationship_existscls, from_class: str, to_class: str, rel_namebool
     # TODO: pytest
        Return True if a relationship with the specified name exists between the two given Classes,
        in the specified direction

        :param from_class:  Name of one existing Class node (blanks allowed in name)
        :param to_class:    Name of another existing Class node (blanks allowed in name)
        :param rel_name:    Name of the relationship(s) to delete,
                                if found in the from -> to direction (blanks allowed in name)
        :return:
        
nameargumentsreturns
get_class_instancescls, class_name: str, leaf_only=False[str]
        Get the names of all Classes that are, directly or indirectly, instances of the given Class,
        i.e. pointing to that node thru a series of 1 or more "INSTANCE_OF" relationships;
        if leaf_only is True, then only as long as they are leaf nodes (with no other Class
        that is an instance of them.)

        :param class_name:  Name of the Class for which we want to find
                            other Classes that are an instance of it
        :param leaf_only:   If True, only return the leaf nodes (those that
                            don't have other Classes that are instances of them)
        :return:            A list of Class names
        
nameargumentsreturns
get_linked_class_namescls, class_name: str, rel_name: str, enforce_unique=FalseUnion[str, List[str]]
        Given a Class, specified by its name, locate and return the name(s) of the other Class(es)
        that it's linked to by means of the relationship with the specified name.
        Typically, the result will contain no more than 1 name, but it could be more;
        it's probably a bad design to use the same relationship name to connect a class to multiple other classes
        (though currently allowed.)
        Relationships are followed in the OUTbound direction only.

        :param class_name:      Name of a Class in the schema
        :param rel_name:        Name of relationship to follow (in the OUTbound direction) from the above Class
        :param enforce_unique:  If True, it raises an Exception if the number of results isn't exactly one

        :return:                If enforce_unique is True, return a string with the class name;
                                otherwise, return a list of names (typically just one)
        
nameargumentsreturns
get_class_relationshipscls, schema_id:int, link_dir="BOTH", omit_instance=FalseUnion[dict, list]
        Fetch and return the names of all the relationship (both inbound and outbound)
        attached to the given Class.
        Treat separately the inbound and the outbound ones.

        :param schema_id:       An integer to identify the desired Class
        :param link_dir:        Desired direction(s) of the relationships; one of "BOTH" (default), "IN" or "OUT"
        :param omit_instance:   If True, the common outbound relationship "INSTANCE_OF" is omitted

        :return:                If link_dir is "BOTH", return a dictionary of the form
                                    {"in": list of inbound-relationship names,
                                     "out": list of outbound-relationship names}
                                Otherwise, just return the inbound or outbound list, based on the value of link_dir
        
nameargumentsreturns
get_class_outbound_datacls, class_neo_id:int, omit_instance=Falsedict
        Efficient all-at-once query to fetch and return the names of all the outbound relationship
        attached to the given Class, as well as the names of the other Classes on the other side of those links.

        IMPORTANT: it's probably a bad design to use the same relationship name to connect a class
        to multiple other classes.  Though currently allowed in the Schema, this particular method
        assumes - and enforces - uniqueness

        :param class_neo_id:    An integer to identify the desired Class
        :param omit_instance:   If True, the common outbound relationship "INSTANCE_OF" is omitted

        :return:                A (possibly empty) dictionary,
                                    where the keys are the name of outbound relationships,
                                    and the values are the names of the Class nodes on the other side of those links.
                                    An Exception will be raised if link names are not unique [though currently allowed by the Schema]
                                    EXAMPLE: {'IS_ATTENDED_BY': 'doctor', 'HAS_RESULT': 'result'}
        

PROPERTIES RELATED

nameargumentsreturns
get_class_properties_fastcls, class_neo_id: int, include_ancestors=False, sort_by_path_len=False[str]
        Faster version of get_class_properties()  [Using class_neo_id]

        Return the list of all the names of the Properties associated with the given Class
        (including those inherited thru ancestor nodes by means of "INSTANCE_OF" relationships,
        if include_ancestors is True),
        sorted by the schema-specified position (or, optionally, by path length)

        :param class_neo_id:        Integer with the Neo4j ID of a Class node
        :param include_ancestors:   If True, also include the Properties attached to Classes that are ancestral
                                    to the given one by means of a chain of outbound "INSTANCE_OF" relationships
                                    Note: the sorting by relationship index won't mean much if ancestral nodes are included,
                                          with their own indexing of relationships; if order matters in those cases, use the
                                          "sort_by_path_len" argument, below
        :param sort_by_path_len:    Only applicable if include_ancestors is True.
                                    If provided, it must be either "ASC" or "DESC", and it will sort the results by path length
                                    (either ascending or descending), before sorting by the schema-specified position for each Class.
                                    Note: with "ASC", the immediate Properties of the given Class will be listed first

        :return:                    A list of the Properties of the specified Class (including indirectly, if include_ancestors is True)
        
nameargumentsreturns
get_class_propertiescls, schema_id: int, include_ancestors=False, sort_by_path_len=Falselist
        TODO: maybe phase out in favor of get_class_properties_fast()

        Return the list of all the names of the Properties associated with the given Class
        (including those inherited thru ancestor nodes by means of "INSTANCE_OF" relationships,
        if include_ancestors is True),
        sorted by the schema-specified position (or, optionally, by path length)

        :param schema_id:           Integer with the ID of a Class node
        :param include_ancestors:   If True, also include the Properties attached to Classes that are ancestral
                                    to the given one by means of a chain of outbound "INSTANCE_OF" relationships
                                    Note: the sorting by relationship index won't mean much if ancestral nodes are included,
                                          with their own indexing of relationships; if order matters in those cases, use the
                                          "sort_by_path_len" argument, below
        :param sort_by_path_len:    Only applicable if include_ancestors is True.
                                    If provided, it must be either "ASC" or "DESC", and it will sort the results by path length
                                    (either ascending or descending), before sorting by the schema-specified position for each Class.
                                    Note: with "ASC", the immediate Properties of the given Class will be listed first

        :return:                    A list of the Properties of the specified Class (including indirectly, if include_ancestors is True)
        
nameargumentsreturns
add_properties_to_classcls, class_node = None, class_id = None, property_list = Noneint
        Add a list of Properties to the specified (ALREADY-existing) Class.
        The properties are given an inherent order (an attribute named "index", starting at 1),
        based on the order they appear in the list.
        If other Properties already exist, the existing numbering gets extended.
        TODO: Offer a way to change the order of the Properties,
              maybe by first deleting all Properties and then re-adding them

        NOTE: if the Class doesn't already exist, use create_class_with_properties() instead;
              attempting to add properties to an non-existing Class will result in an Exception

        :param class_node:      Either an integer with the internal database ID of an existing Class node,
                                    (or a string with its name - TODO: add support for this option)
        :param class_id:        Integer with the schema_id of the Class to which attach the given Properties
                                TODO: remove

        :param property_list:   A list of strings with the names of the properties, in the desired order.
                                    Whitespace in any of the names gets stripped out.
                                    If any name is a blank string, an Exception is raised
                                    If the list is empty, an Exception is raised
        :return:                The number of Properties added
        
nameargumentsreturns
create_class_with_propertiescls, name :str, property_list: [str], code=None, strict=False, class_to_link_to=None, link_name="INSTANCE_OF", link_dir="OUT"(int, int)
        Create a new Class node, with the specified name, and also create the specified Properties nodes,
        and link them together with "HAS_PROPERTY" relationships.

        Return the internal database ID and the auto-incremented unique ID ("scheme ID") assigned to the new Class.
        Each Property node is also assigned a unique "scheme ID";
        the "HAS_PROPERTY" relationships are assigned an auto-increment index,
        representing the default order of the Properties.

        If a class_to_link_to name is specified, link the newly-created Class node to that existing Class node,
        using an outbound relationship with the specified name.  Typically used to create "INSTANCE_OF"
        relationships from new Classes.

        If a Class with the given name already exists, nothing is done,
        and an Exception is raised.

        NOTE: if the Class already exists, use add_properties_to_class() instead

        :param name:            String with name to assign to the new class
        :param property_list:   List of strings with the names of the Properties, in their default order (if that matters)
        :param code:            Optional string indicative of the software handler for this Class and its subclasses
        :param strict:          If True, the Class will be of the "S" (Strict) type;
                                    otherwise, it'll be of the "L" (Lenient) type

        :param class_to_link_to: If this name is specified, and a link_to_name (below) is also specified,
                                    then create an OUTBOUND relationship from the newly-created Class
                                    to this existing Class
        :param link_name:       Name to use for the above relationship, if requested.  Default is "INSTANCE_OF"
        :param link_dir:        Desired direction(s) of the relationships: either "OUT" (default) or "IN"

        :return:                If successful, the pair (internal ID, integer "schema_id" assigned to the new Class);
                                otherwise, raise an Exception
        
nameargumentsreturns
remove_property_from_classcls, class_id: int, property_id: intNone
        Take out the specified (single) Property from the given Class.
        If the Class or Property was not found, or if the Property could not be removed, an Exception is raised

        :param class_id:    The schema ID of the Class node
        :param property_id: The schema ID of the Property node
        :return:            None
        
nameargumentsreturns
get_schema_codecls, class_name: strstr
        Obtain the "schema code" of a Class, specified by its name.
        The "schema code" is an optional but convenient text code,
        stored either on a Class node, or on any of its ancestors by way of "INSTANCE_OF" relationships

        :return:    A string with the Schema code (empty string if not found)
                    EXAMPLE: "i"
        
nameargumentsreturns
get_schema_idcls, schema_code: strint
        Get the Schema ID most directly associated to the given Schema Code

        :return:    An integer with the Schema ID (or -1 if not present)
        

DATA NODES

nameargumentsreturns
all_propertiescls, label, primary_key_name, primary_key_value[str]
        Return the list of the *names* of all the Properties associated with the given DATA node,
        based on the Schema it is associated with, sorted their by schema-specified position.
        The desired node is identified by specifying which one of its attributes is a primary key,
        and providing a value for it.

        IMPORTANT : this function returns the NAMES of the Properties; not their values

        :param label:
        :param primary_key_name:
        :param primary_key_value:
        :return:
        
nameargumentsreturns
get_data_node_internal_idcls, item_id: intint
        Returns the internal database ID of the given data node,
        specified by its value of the item_id attribute

        :param item_id: Integer to identify a data node by the value of its item_id attribute
        :return:        The internal database ID of the specified data node
        
nameargumentsreturns
get_data_node_idcls, key_value, key_name="item_id"int
        Get the internal database ID of a data node given some other primary key

        :return:   An integer with the Neo4j ID of the data node
        
nameargumentsreturns
fetch_data_nodecls, item_id = None, internal_id = None, labels=None, properties=NoneUnion[dict, None]
        Return a dictionary with all the key/value pairs of the attributes of given data node

        See also locate_node()

        :param item_id:     The "item_id" field to uniquely identify the data node
        :param internal_id: OPTIONAL alternate way to specify the data node;
                                if present, it takes priority
        :param labels:      OPTIONAL (generally, redundant) ways to locate the data node
        :param properties:  OPTIONAL (generally, redundant) ways to locate the data node

        :return:            A dictionary with all the key/value pairs, if found; or None if not
                                TODO: {} is actually currently returned, till get_nodes() gets fixed
        
nameargumentsreturns
locate_nodecls, node_id: Union[int, str], id_type=None, labels=None, dummy_node_name="n"CypherMatch
        EXPERIMENTAL - a generalization of fetch_data_node()

        Return the "match" structure to later use to locate a node identified
        either by its internal database ID (default), or by a primary key (with optional label.)

        No database operation is actually performed.

        :param node_id: This is understood be the Neo4j ID, unless an id_type is specified
        :param id_type: For example, "item_id";
                            if not specified, the node ID is assumed to be Neo4j ID's
        :param labels:  (OPTIONAL) Labels - a string or list/tuple of strings - for the node
        :param dummy_node_name: (OPTIONAL) A string with a name by which to refer to the node (by default, "n")

        :return:        A "CypherMatch" object
        
nameargumentsreturns
data_nodes_of_classcls, class_name[int]
        Return the Item ID's of all the Data Nodes of the given Class
        TODO: offer to optionally use a label
        TODO: switch to returning the internal database ID's

        :param class_name:
        :return:            Return the Item ID's of all the Data Nodes of the given Class
        
nameargumentsreturns
count_data_nodes_of_classcls, class_id: Union[int, str][int]
        Return the count of all the Data Nodes attached to the given Class

        :param class_id:    Either an integer with the internal database ID of an existing Class node,
                                or a string with its name
        :return:            The count of all the Data Nodes attached to the given Class
        
nameargumentsreturns
allowable_propscls, class_internal_id: int, requested_props: dict, silently_drop: bool, schema_cache=Nonedict
        If any of the properties in the requested list of properties is not a declared (and thus allowed) Schema property,
        then:
            1) if silently_drop is True, drop that property from the returned pared-down list
            2) if silently_drop is False, raise an Exception

        TODO: possibly expand to handle REQUIRED properties

        :param class_internal_id:    The internal ID of a Schema Class node
        :param requested_props: A dictionary of properties one wishes to assign to a new data node, if the Schema allows
        :param silently_drop:   If True, any requested properties not allowed by the Schema are simply dropped;
                                    otherwise, an Exception is raised if any property isn't allowed
        :param schema_cache:    (OPTIONAL) "SchemaCache" object

        :return:                A possibly pared-down version of the requested_props dictionary
        
nameargumentsreturns
create_data_nodecls, class_node: Union[int, str], properties = None, extra_labels = None, assign_uri=False, new_uri=None, silently_drop=Falseint
        A newer version of the deprecated add_data_point_OLD()

        Create a new data node, of the type indicated by specified Class,
        with the given (possibly none) attributes and label(s);
        if no labels are given, the name of the Class is used as a label.

        The new data node, if successfully created, will optionally be assigned
        a passed URI value, or a unique auto-gen value, for its field item_id.

        If the requested Class doesn't exist, an Exception is raised

        If the data node needs to be created with links to other existing data nodes,
        use add_data_node_with_links() instead

        Note: the responsibility for picking a URI belongs to the calling function
              (which will typically make use of a namespace)   TODO: finish the rollout of this approach

        :param class_node:  Either an integer with the internal database ID of an existing Class node,
                                or a string with its name
        :param properties:  (Optional) Dictionary with the properties of the new data node.
                                EXAMPLE: {"make": "Toyota", "color": "white"}
        :param extra_labels:(Optional) String, or list/tuple of strings, with label(s) to assign to the new data node,
                                IN ADDITION TO the Class name (which is always used as label)

        :param assign_uri:  (DEPRECATED) If True, the new node is given an extra attribute named "item_id",
                                with a unique auto-increment value in the "data_node" namespace,
                                as well an extra attribute named "schema_code"
                                (TODO: drop)

        :param new_uri:     If new_uri is provided, then a field called "item_id" (TODO: rename to "uri")
                                is set to that value;
                                also, an extra attribute named "schema_code" gets set
                                # TODO: "schema_code" should perhaps be responsibility of the higher layer

        :param silently_drop: If True, any requested properties not allowed by the Schema are simply dropped;
                                otherwise, an Exception is raised if any property isn't allowed
                                Note: only applicable for "Strict" schema - with a "Lenient" schema anything goes

        :return:            The internal database ID of the new data node just created
nameargumentsreturns
add_data_node_mergecls, class_internal_id, properties = None, labels = None, silently_drop=False, schema_cache=None(int, bool)
        Similar to add_data_point_new(), but a new data node gets created only if
        there's no other data node with the same labels and allowed properties

        :param class_internal_id:   The internal database ID of the Class node for the data node
        :param properties:          An optional dictionary with the properties of the new data node.
                                        EXAMPLE: {"make": "Toyota", "color": "white"}
        :param labels:              OPTIONAL string, or list of strings, with label(s) to assign to the new data node;
                                        if not specified, the Class name is used
        :param silently_drop:       If True, any requested properties not allowed by the Schema are simply dropped;
                                        otherwise, an Exception is raised if any property isn't allowed
        :param schema_cache:        (OPTIONAL) "SchemaCache" object

        :return:                    A pair with:
                                        1) The internal database ID of either an existing data node or of a new one just created
                                        2) True if a new data node was created, or False if not (i.e. an existing one was found)
        
nameargumentsreturns
add_data_column_mergecls, class_internal_id: int, property_name: str, value_list: listdict
        Add a data column (i.e. a set of single-property data nodes).
        Individual nodes are created only if there's no other data node with the same property/value

        TODO: this is a simple approach; introduce a more efficient one, possibly using APOC

        :param class_internal_id:   The internal database ID of the Class node for the data nodes
        :param property_name:       The name of the data column
        :param value_list:          The data column as a list
        :return:                    A dictionary with 2 keys - "new_nodes" and "old_nodes"
                                        TODO: rename "old_nodes" to "present_nodes" (or "existing_nodes")
        
nameargumentsreturns
add_data_node_with_linkscls, class_name = None, class_internal_id = None, properties = None, labels = None, links = None, assign_item_id=False, new_item_id=Noneint
        This is NeoSchema's counterpart of NeoAccess.create_node_with_links()

        Add a new data node, of the Class specified by its name,
        with the given (possibly none) attributes and label(s),
        optionally linked to other, already existing, DATA nodes.

        If the specified Class doesn't exist, or doesn't allow for Data Nodes, an Exception is raised.

        The new data node, if successfully created:
            1) will be given the Class name as a label, unless labels are specified
            2) will optionally be assigned an "item_id" unique value
               that is either automatically assigned or passed.

        EXAMPLES:   add_data_node_with_links(class_name="Cars",
                                              properties={"make": "Toyota", "color": "white"},
                                              links=[{"internal_id": 123, "rel_name": "OWNED_BY", "rel_dir": "IN"}])

        TODO: verify the all the passed attributes are indeed properties of the class (if the schema is Strict)
        TODO: verify that required attributes are present
        TODO: verify that all the requested links conform to the Schema
        TODO: invoke special plugin-code, if applicable???
        TODO: maybe rename to add_data_node()

        :param class_name:  The name of the Class that this new data node is an instance of.
                                Also use to set a label on the new node, if labels isn't specified
        :param class_internal_id: OPTIONAL alternative to class_name.  If both specified,
                                class_internal_id prevails
                            TODO: merge class_name and class_internal_id into class_node, as done
                                  for create_data_node()
        :param properties:  An optional dictionary with the properties of the new data node.
                                EXAMPLE: {"make": "Toyota", "color": "white"}
        :param labels:      OPTIONAL string, or list of strings, with label(s) to assign to the new data node;
                                if not specified, use the Class name.  TODO: ALWAYS include the Class name, as done in create_data_node()
        :param links:       OPTIONAL list of dicts identifying existing nodes,
                                and specifying the name, direction and optional properties
                                to give to the links connecting to them;
                                use None, or an empty list, to indicate if there aren't any
                                Each dict contains the following keys:
                                    "internal_id"   REQUIRED - to identify an existing node
                                    "rel_name"      REQUIRED - the name to give to the link
                                    "rel_dir"       OPTIONAL (default "OUT") - either "IN" or "OUT" from the new node
                                    "rel_attrs"     OPTIONAL - A dictionary of relationship attributes

        :param assign_item_id:  If True, the new node is given an extra attribute named "item_id",
                                    with a unique auto-increment value, as well an extra attribute named "schema_code".
                                    Default is False
                                    TODO: rename to assign_uri (or perhaps assign_token)
        :param new_item_id:     Normally, the Item ID is auto-generated, but it can also be provided (Note: MUST be unique)
                                    If new_item_id is provided, then assign_item_id is automatically made True
                                    TODO: rename to new_uri (or perhaps new_token)

        :return:                If successful, an integer with the internal database ID of the node just created;
                                    otherwise, an Exception is raised
        
nameargumentsreturns
add_data_point_fast_OBSOLETEcls, class_name="", schema_id=None, properties=None, labels=None, connected_to_neo_id=None, rel_name=None, rel_dir="OUT", rel_prop_key=None, rel_prop_value=None, assign_item_id=False, new_item_id=Noneint
        TODO: OBSOLETED BY add_data_node_with_links() - TO DITCH *AFTER* add_data_node_with_links() gets link validation!
        A faster version of add_data_point()
        Add a new data node, of the Class specified by name or ID,
        with the given (possibly none) attributes and label(s),
        optionally linked to another, already existing, DATA node.

        The new data node, if successfully created, will be assigned a unique value for its field item_id
        If the requested Class doesn't exist, an Exception is raised

        NOTE: if the new node requires MULTIPLE links to existing data points, use add_and_link_data_point() instead

        EXAMPLES:   add_data_point(class_name="Cars", data_dict={"make": "Toyota", "color": "white"}, labels="car")
                    add_data_point(schema_id=123,     data_dict={"make": "Toyota", "color": "white"}, labels="car",
                                   connected_to_id=999, connected_to_labels="salesperson", rel_name="SOLD_BY", rel_dir="OUT")
                    assuming there's an existing class named "Cars" and an existing data point with item_id = 999, and label "salesperson"

        TODO: verify the all the passed attributes are indeed properties of the class (if the schema is Strict)
        TODO: verify that required attributes are present
        TODO: invoke special plugin-code, if applicable

        :param class_name:      The name of the Class that this new data point is an instance of
        :param schema_id:       Alternate way to specify the Class; if both present, class_name prevails

        :param properties:      An optional dictionary with the properties of the new data point.   TODO: NEW - changed name
                                    EXAMPLE: {"make": "Toyota", "color": "white"}
        :param labels:          String or list of strings with label(s) to assign to the new data node;
                                    if not specified, use the Class name

        :param connected_to_neo_id: Int or None.  To optionally specify another (already existing) DATA node
                                        to connect the new node to, specified by its Neo4j
                                        EXAMPLE: the item_id of a data point representing a particular salesperson or dealership

        The following group only applicable if connected_to_id isn't None
        :param rel_name:        Str or None.  EXAMPLE: "SOLD_BY"
        :param rel_dir:         Str or None.  Either "OUT" (default) or "IN"
        :param rel_prop_key:    Str or None.  Ignored if rel_prop_value is missing
        :param rel_prop_value:  Str or None.  Ignored if rel_prop_key is missing

        :param assign_item_id:  If True, the new node is given an extra attribute named "item_id" with a unique auto-increment value
        :param new_item_id:     Normally, the Item ID is auto-generated, but it can also be provided (Note: MUST be unique)
                                    If new_item_id is provided, then assign_item_id is automatically made True

        :return:                If successful, an integer with the Neo4j ID
                                    of the node just created;
                                    otherwise, an Exception is raised
        
nameargumentsreturns
add_data_point_OLDcls, class_name="", schema_id=None, data_dict=None, labels=None, connected_to_id=None, connected_to_labels=None, rel_name=None, rel_dir="OUT", rel_prop_key=None, rel_prop_value=None, new_item_id=None, return_item_ID=Trueint

nameargumentsreturns
add_and_link_data_point_OBSOLETEcls, class_name: str, connected_to_list: [tuple], properties=None, labels=None, assign_item_id=Falseint
        TODO: OBSOLETED BY add_data_node_with_links() - TO DITCH *AFTER* add_data_node_with_links() gets link validation!
        Create a new data node, of the Class with the given name,
        with the specified optional labels and properties,
        and link it to each of all the EXISTING nodes
        specified in the (possibly empty) list connected_to_list,
        using the various relationship names specified inside that list.

        All the relationships are understood to be OUTbound from the newly-created node -
        and they must be present in the Schema, or an Exception will be raised.

        If the requested Class doesn't exist, an Exception is raised

        The new data node optionally gets assigned a unique "item_id" value (TODO: make optional)

        EXAMPLE:
            add_and_link_data_point(
                                class_name="PERSON",
                                properties={"name": "Julian", "city": "Berkeley"},
                                connected_to_list=[ (123, "IS_EMPLOYED_BY") , (456, "OWNS") ]
            )

        Note: this is the Schema layer's counterpart of NeoAccess.create_node_with_children()

        :param class_name:          Name of the Class specifying the schema for this new data point
        :param connected_to_list:   A list of pairs (Neo4j ID value, relationship name)
        :param properties:          A dictionary of attributes to give to the new node
        :param labels:              OPTIONAL string or list of strings with label(s) to assign to new data node;
                                        if not specified, use the Class name
        :param assign_item_id:      If True, the new node is given an extra attribute named "item_id" with a unique auto-increment value

        :return:                    If successful, an integer with Neo4j ID of the node just created;
                                        otherwise, an Exception is raised
        
nameargumentsreturns
register_existing_data_nodecls, class_name="", schema_id=None, existing_neo_id=None, new_item_id=Noneint
        Register (declare to the Schema) an existing data node with the Schema Class specified by its name or ID.
        An item_id is generated for the data node and stored on it; likewise, for a schema_code (if applicable).
        Return the newly-assigned item_id

        EXAMPLES:   register_existing_data_node(class_name="Chemicals", existing_neo_id=123)
                    register_existing_data_node(schema_id=19, existing_neo_id=456)

        TODO: verify the all the passed attributes are indeed properties of the class (if the schema is Strict)
        TODO: verify that required attributes are present
        TODO: invoke special plugin-code, if applicable

        :param class_name:      The name of the Class that this new data node is an instance of
        :param schema_id:       Alternate way to specify the Class; if both present, class_name prevails

        :param existing_neo_id: Internal ID to identify the node to register with the above Class.
                                TODO: expand to use the match() structure
        :param new_item_id:     OPTIONAL. Normally, the Item ID is auto-generated,
                                but it can also be provided (Note: MUST be unique)

        :return:                If successful, an integer with the auto-increment "item_id" value of the node just created;
                                otherwise, an Exception is raised
        
nameargumentsreturns
update_data_nodecls, data_node :Union[int, str], set_dict :dict , drop_blanks = Trueint
        Update, possibly adding and/or dropping fields, the properties of an existing Data Node

        :param data_node:   Either an integer with the internal database ID, or a string with a URI value
        :param set_dict:    A dictionary of field name/values to create/update the node's attributes
                                (note: blanks ARE allowed in the keys)
        :param drop_blanks: If True, then any blank field is interpreted as a request to drop that property
                                (as opposed to setting its value to "")
        :return:            The number of properties set or removed;
                                if the record wasn't found, or an empty set_dict was passed, return 0
                                Important: a property is counted as "set" even if the new value is
                                           identical to the old value!
nameargumentsreturns
delete_data_nodecls, node_id=None, uri=None, class_node=None, labels=NoneNone
        Delete the given data node.
        If no node gets deleted, or if more than 1 get deleted, an Exception is raised

        :param node_id:     An integer with the internal database ID of an existing data node
        :param uri:         An alternate way to refer to the node.  TODO: implement
        :param class_node:  NOT IN CURRENT USE.  Specify the Class to which this node belongs TODO: implement
        :param labels:      (OPTIONAL) String or list of strings.
                                If passed, each label must be present in the node, for a match to occur
                                (no problem if the node also includes other labels not listed here.)
                                Generally, redundant, as a precaution against deleting wrong node
        :return:            None
        
nameargumentsreturns
delete_data_pointcls, item_id: int, labels=Noneint
        Delete the given data point.  TODO: obsolete in favor of delete_data_node()

        :param item_id:
        :param labels:      OPTIONAL (generally, redundant)
        :return:            The number of nodes deleted (possibly zero)
        
nameargumentsreturns
add_data_relationship_OLDcls, from_id: Union[int, str], to_id: Union[int, str], rel_name: str, rel_props = None, labels_from=None, labels_to=None, id_type=NoneNone
        -> Maybe not really needed.  IF POSSIBLE, USE add_data_relationship() INSTEAD
        TODO: possibly ditch, in favor of add_data_relationship()

        Add a new relationship with the given name, from one to the other of the 2 given DATA nodes.
        The new relationship must be present in the Schema, or an Exception will be raised.

        The data nodes may be identified either by their Neo4j ID's, or by a primary key (with optional label.)

        Note that if a relationship with the same name already exists between the data nodes exists,
        nothing gets created (and an Exception is raised)

        :param from_id:     The ID of the data node at which the new relationship is to originate;
                                this is understood be the Neo4j ID, unless an id_type is specified
        :param to_id:       The ID of the data node at which the new relationship is to end;
                                this is understood be the Neo4j ID, unless an id_type is specified
        :param rel_name:    The name to give to the new relationship between the 2 specified data nodes
        :param rel_props:   TODO: not currently used.  Unclear what multiple calls would do in this case
        :param labels_from: (OPTIONAL) Labels on the 1st data node
        :param labels_to:   (OPTIONAL) Labels on the 2nd data node
        :param id_type:     For example, "item_id";
                            if not specified, all the node ID's are assumed to be Neo4j ID's

        :return:            None.  If the specified relationship didn't get created (for example,
                            in case the the new relationship doesn't exist in the Schema), raise an Exception
        
nameargumentsreturns
add_data_relationshipcls, from_id:int, to_id: int, rel_name: str, rel_props = NoneNone
        Simpler (and possibly faster) version of add_data_relationship()

        Add a new relationship with the given name, from one to the other of the 2 given data nodes,
        identified by their Neo4j ID's.
        The requested new relationship MUST be present in the Schema, or an Exception will be raised.

        Note that if a relationship with the same name already exists between the data nodes exists,
        nothing gets created (and an Exception is raised)

        :param from_id: The Neo4j ID of the data node at which the new relationship is to originate
                                TODO: also allow primary keys, as done in class_of_data_node()
        :param to_id:   The Neo4j ID of the data node at which the new relationship is to end
                                TODO: also allow primary keys, as done in class_of_data_node()
        :param rel_name:    The name to give to the new relationship between the 2 specified data nodes
                                IMPORTANT: it MUST match an existing relationship in the Schema,
                                           between the respective Classes of the 2 data nodes
        :param rel_props:   TODO: not currently used.  Unclear what multiple calls would do in this case

        :return:            None.  If the specified relationship didn't get created (for example,
                                in case the the new relationship doesn't exist in the Schema), raise an Exception
        
nameargumentsreturns
remove_data_relationshipcls, from_item_id: int, to_item_id: int, rel_name: str, labels=NoneNone
        Drop the relationship with the given name, from one to the other of the 2 given data nodes.
        Note: the data nodes are left untouched.
        If the specified relationship didn't get deleted, raise an Exception

        TODO: first verify that the relationship is optional in the schema???
        TODO: migrate from "item_id" values to also internal database ID's, as done in class_of_data_node()

        :param from_item_id:The "item_id" value of the data node at which the relationship originates
        :param to_item_id:  The "item_id" value of the data node at which the relationship ends
        :param rel_name:    The name of the relationship to delete
        :param labels:      OPTIONAL (generally, redundant).  Labels required to be on both nodes

        :return:            None.  If the specified relationship didn't get deleted, raise an Exception
        
nameargumentsreturns
remove_multiple_data_relationshipscls, node_id: Union[int, str], rel_name: str, rel_dir: str, labels=NoneNone
     TODO: test
        Drop all the relationships with the given name, from or to the given data node.
        Note: the data node is left untouched.

        IMPORTANT: this function cannot be used to remove relationship involving any Schema node

        :param node_id:     The internal database ID or name of the data node of interest
        :param rel_name:    The name of the relationship(s) to delete
        :param rel_dir:     Either 'IN', 'OUT', or 'BOTH'
        :param labels:      [OPTIONAL]
        :return:            None
        
nameargumentsreturns
class_of_data_nodecls, node_id: int, id_type=None, labels=Nonestr
        Return the name of the Class of the given data node: identified
        either by its Neo4j ID (default), or by a primary key (with optional label)

        :param node_id:     Either an internal database ID or a primary key value
        :param id_type:     OPTIONAL - name of a primary key used to identify the data node
        :param labels:      Optional string, or list/tuple of strings, with Neo4j labels
        :return:            A string with the name of the Class of the given data node
        
nameargumentsreturns
data_nodes_lacking_schemacls
        Locate and return all Data Nodes that aren't associated to any Class
        TODO: generalize the "BA" label
        TODO: test

        :return:
        

DATA IMPORT

nameargumentsreturns
import_json_datacls, json_str: str, class_name: str, parse_only=False, provenance=NoneUnion[None, int, List[int]]
        Import the data specified by a JSON string into the database -
        but only the data that is described in the existing Schema;
        anything else is silently ignored.

        CAUTION: A "postorder" approach is followed: create subtrees first (with recursive calls), then create the root last;
        as a consequence, in case of failure mid-import, there's no top root, and there could be several fragments.
        A partial import might need to be manually deleted.
        TODO: maintain a list of all created nodes - so as to be able to delete them all in case of failure.

        :param json_str:    A JSON string representing (at the top level) an object or a list to import
        :param class_name:  Name of Schema class to use for the top-level element(s)
        :param parse_only:  Flag indicating whether to stop after the parsing (i.e. no database import)
        :param provenance:  Metadata (such as a file name) to store in the "source" attribute
                                of a special extra node ("Import Data")

        :return:
        
nameargumentsreturns
create_data_nodes_from_python_datacls, data, class_name: str, provenance=None[int]
        Import the data specified by the "data" python structure into the database -
        but only the data that is described in the existing Schema;
        anything else is silently ignored.
        For additional notes, see import_json_data()

        :param data:        A python dictionary or list, with the data to import
        :param class_name:  The name of the Schema Class for the root node(s) of the imported data
        :param provenance:  Optional string to be stored in a "source" attribute
                                in a special "Import Data" node for metadata about the import

        :return:            List (possibly empty) of Neo4j ID's of the root node(s) created

        TODO:   * The "Import Data" Class must already be in the Schema; should automatically add it, if not already present
                * DIRECTION OF RELATIONSHIP (cannot be specified by Python dict/JSON)
                * LACK OF "Import Data" node (ought to be automatically created if needed)
                * LACK OF "BA" (or "DATA"?) labels being set
                * INABILITY TO LINK TO EXISTING NODES IN DBASE (try using: "item_id": some_int  as the only property in nodes to merge)
                * HAZY responsibility for "schema_code" (set correctly for all nodes); maybe ditch to speed up execution
                * OFFER AN OPTION TO IGNORE BLANK STRINGS IN ATTRIBUTES
                * INTERCEPT AND BLOCK IMPORTS FROM FILES ALREADY IMPORTED
                * issue some report about any part of the data that doesn't match the Schema, and got silently dropped
        
nameargumentsreturns
create_tree_from_dictcls, d: dict, class_name: str, level=1, cache=NoneUnion[int, None]
        Add a new data node (which may turn into a tree root) of the specified Class,
        with data from the given dictionary:
            1) literal values in the dictionary are stored as attributes of the node, using the keys as names
            2) other values (such as dictionaries or lists) are recursively turned into subtrees,
               linked from the new data node through outbound relationships using the dictionary keys as names

        Return the Neo4j ID of the newly created root node,
        or None is nothing is created (this typically arises in recursive calls that "skip subtrees")

        IMPORTANT:  any part of the data that doesn't match the Schema,
                    gets silently dropped.  TODO: issue some report about anything that gets dropped

        EXAMPLES:
        (1) {"state": "California", "city": "Berkeley"}
            results in the creation of a new node, with 2 attributes, named "state" and "city"

        (2) {"name": "Julian", "address": {"state": "California", "city": "Berkeley"}}
            results in the creation of 2 nodes, namely the tree root (with a single attribute "name"), with
            an outbound link named "address" to another node (the subtree) that has the "state" and "city" attributes

        (3) {"headquarter_state": [{"state": "CA"}, {"state": "NY"}, {"state": "FL"}]}
            results in the creation of a node (the tree root), with no attributes, and 3 links named "headquarter_state" to,
            respectively, 3 nodes - each of which containing a "state" attribute

        (4) {"headquarter_state": ["CA", "NY", "FL"]}
            similar to (3), above, but the children nodes will use the default attribute name "value"

        :param d:           A dictionary with data from which to create a tree in the database
        :param class_name:  The name of the Schema Class for the root node(s) of the imported data
        :param level:       The level of the recursive call (used for debug printing)
        :return:            The Neo4j ID of the newly created node,
                                or None is nothing is created (this typically arises in recursive calls that "skip subtrees")
        
nameargumentsreturns
create_trees_from_listcls, l: list, class_name: str, level=1, cache=None[int]
        Add a set of new data nodes (the roots of the trees), all of the specified Class,
        with data from the given list.
        Each list elements MUST be a literal, or dictionary or a list:
            - if a literal, it first gets turned into a dictionary of the form {"value": literal_element};
            - if a dictionary, it gets processed by create_tree_from_dict()
            - if a list, it generates a recursive call

        Return a list of the Neo4j ID of the newly created nodes.

        IMPORTANT:  any part of the data that doesn't match the Schema,
                    gets silently dropped.  TODO: issue some report about that

        EXAMPLE:
            If the Class is named "address" and has 2 properties, "state" and "city",
            then the data:
                    [{"state": "California", "city": "Berkeley"},
                     {"state": "Texas", "city": "Dallas"}]
            will give rise to 2 new data nodes with label "address", and each of them having a "SCHEMA"
            link to the shared Class node.

        :param l:           A list of data from which to create a set of trees in the database
        :param class_name:  The name of the Schema Class for the root node(s) of the imported data
        :param level:       The level of the recursive call (used for debug printing)

        :return:            A list of the Neo4j values of the newly created nodes (each of which
                                might be a root of a tree)
        

EXPORT SCHEMA

nameargumentsreturns
export_schemacls{}
        TODO: unit testing
        Export all the Schema nodes and relationships as a JSON string.

        IMPORTANT:  APOC must be activated in the database, to use this function.
                    Otherwise it'll raise an Exception

        :return:    A dictionary specifying the number of nodes exported,
                    the number of relationships, and the number of properties,
                    as well as a "data" field with the actual export as a JSON string
        

URI

nameargumentsreturns
generate_uricls, prefix, namespace, suffixstr
        Generate a URI (or fragment thereof, aka "token"),
        using the given prefix and suffix; 
        the middle part is a unique auto-increment value (separately maintained
        in various groups, or "namespaces".)
        
        EXAMPLE:  generate_uri("doc.", "documents", ".new") might produce "doc.3.new"
nameargumentsreturns
next_autoincrementcls, namespace: str, advance=1int
        This utilizes an ATOMIC database operation to both read and advance the autoincrement counter,
        based on a (single) node with label `Schema Autoincrement`
        and an attribute indicating the desired namespace (group);
        if no such node exists (for example, after a new installation), it gets created, and 1 is returned.

        Note that the returned number (or sequence of numbers, if advance > 1)
        is de-facto "permanently reserved" on behalf of the calling function,
        and can't be used by any other competing thread, thus avoid concurrency problems (racing conditions)

        :param namespace:   A string used to maintain completely separate groups of auto-increment values;
                                leading/trailing blanks are ignored
        :param advance:     Normally, auto-increment advances by 1 unit, but a different positive integer
                                may be used

        :return:            An integer that is a unique auto-increment for the specified namespace
                                (starting with 1); it's ready-to-use and "reserved", i.e. could be used
                                at any future time
        
nameargumentsreturns
next_available_datanode_idclsint
        Reserve and return the next available auto-increment ID,
        in the separately-maintained group called "data_node".
        This value (currently often referred to as "item_id", and not to be confused
        with the internal ID assigned by Neo4j to each node),
        is meant as a permanent primary key, on which a URI could be based.

        For unique ID's to use on schema nodes, use next_available_schema_id() instead

        :return:    A unique auto-increment integer used for Data nodes
        

PRIVATE METHODS

nameargumentsreturns
valid_schema_idcls, schema_id: intbool
        Check the validity of the passed Schema ID

        :param schema_id:
        :return:
        
nameargumentsreturns
next_available_schema_idclsint
        Return the next available ID for nodes managed by this class
        For unique ID's to use on data nodes, use next_available_datanode_id() instead

        :return:     A unique auto-increment integer used for Schema nodes
        
nameargumentsreturns
debug_printcls, info: str, trim=FalseNone
        If the class' property "debug" is set to True,
        print out the passed info string,
        optionally trimming it, if too long

        :param info:
        :param trim:
        :return:        None
        





Class SchemaCache

    Cached by the Classes' internal database ID

    Used to improve the efficiency of methods that heavily interact with the Schema,
    such as JSON imports.

    Maintain a Python dictionary, whose keys are the internal database IDs of Schema Class nodes.
    Generally, it will be a subset of interest from all the Classes in the database.

    Note: this class gets instantiated, so that it's a local variable and won't cause
          trouble with multi-threading

    TODO:   add a "schema" argument to some NeoSchema methods that interact with the Schema,
            to provide an alternate manner of querying the Schema
            - as currently done by several method
    
nameargumentsreturns
__init__self

nameargumentsreturns
get_all_cached_class_dataself, class_id: intdict
        Return all existed cached data for the specified Class

        :param class_id:    An integer with the database internal ID of the desired Class node
        :return:            A (possibly empty) dict with keys that may include
                                "class_attributes", "class_properties", "out_neighbors"
        
nameargumentsreturns
get_cached_class_dataself, class_id: int, request: strUnion[dict, List[str]]
        Return the requested data for the specified Class.

        If cached values are available, they get used;
        otherwise, they get queried, then cached and returned.

        If request == "class_attributes":
            return the attributes of the requested Class,
            i.e. a dictionary of all the Class node's attributes
            EXAMPLE:  {'name': 'MY CLASS', 'schema_id': 123, 'type': 'L'}

        If request == "class_properties":
            return the properties of the requested Class,
            i.e. the  list of all the names of the Properties associated with the given Class
            EXAMPLE:  ["age", "gender", "weight"]

        If request == "out_neighbors":
            return a dictionary where the keys are the names of the outbound relationships from with the given Class,
            and the values are the names of the Classes on the other side of those relationships
            EXAMPLE:  {'IS_ATTENDED_BY': 'doctor', 'HAS_RESULT': 'result'}

        :param class_id:    An integer with the database internal ID of the desired Class node
        :param request:     A way to specify what to look up.
                                Permissible values: "class_attributes", "class_properties", "out_neighbors"
        :return: