Source code for py2neo

#!/usr/bin/env python
# -*- encoding: utf-8 -*-

# Copyright 2011-2021, Nigel Small
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""
Py2neo consists of several distinct regions of API, the heart of which
is the Graph API. This has evolved from the original, foundational API
included with early versions of the library, and remains relevant for
general purpose use today.

The :class:`.Graph` class represents a graph database exposed by a
Neo4j service running on a single instance or cluster, and which
provides access to a large portion of the most commonly used py2neo
features. The full DBMS is represented by a :class:`.GraphService`
object.

For convenience, all core functions and classes are exported from the
``py2neo`` root namespace. This includes all connectivity and database
management functionality as well as entity matching and core errors.
"""

from __future__ import absolute_import, print_function


__all__ = [

    # Project metadata
    "__author__",
    "__copyright__",
    "__email__",
    "__license__",
    "__package__",
    "__version__",

    # Basic connectivity
    "DEFAULT_PROTOCOL",
    "DEFAULT_SECURE",
    "DEFAULT_VERIFY",
    "DEFAULT_USER",
    "DEFAULT_PASSWORD",
    "DEFAULT_HOST",
    "DEFAULT_BOLT_PORT",
    "DEFAULT_HTTP_PORT",
    "DEFAULT_HTTPS_PORT",
    "ConnectionProfile",
    "ServiceProfile",

]


from os import getenv

from py2neo.addressing import Address
from py2neo.compat import Mapping, string_types, urlsplit
from py2neo.meta import get_metadata

try:
    from py2neo.database import *
    from py2neo.errors import *
    from py2neo.matching import *
    from py2neo.data import *
except ImportError:
    pass
else:
    __all__ += database.__all__
    __all__ += errors.__all__
    __all__ += matching.__all__
    __all__ += data.__all__


metadata = get_metadata()

__author__ = metadata["author"]
__copyright__ = "2011, {}".format(metadata["author"])
__email__ = metadata["author_email"]
__license__ = metadata["license"]
__package__ = metadata["name"]
__version__ = metadata["version"]


NEO4J_URI = getenv("NEO4J_URI")
NEO4J_AUTH = getenv("NEO4J_AUTH")
NEO4J_SECURE = getenv("NEO4J_SECURE")
NEO4J_VERIFY = getenv("NEO4J_VERIFY")


DEFAULT_PROTOCOL = "bolt"
DEFAULT_SECURE = False
DEFAULT_VERIFY = True
DEFAULT_USER = "neo4j"
DEFAULT_PASSWORD = "password"
DEFAULT_HOST = "localhost"
DEFAULT_BOLT_PORT = 7687
DEFAULT_HTTP_PORT = 7474
DEFAULT_HTTPS_PORT = 7473


[docs] class ConnectionProfile(Mapping): """ Connection details for a Neo4j server. A connection profile holds a set of values that describe how to connect to, and authorise against, a particular Neo4j server. The set of values held within a profile are available as either object attributes (e.g. ``profile.uri``) or sub-items (e.g. ``profile["uri"]``). Profile instances are immutable, so can be safely hashed for inclusion within a set or as dictionary keys. :param profile: The base connection information, provided as a dictionary of settings, an existing :class:`.ConnectionProfile` object or a string URI. This value can also be :const:`None`, in which case default base settings are used. :param settings: Optional set of individual overrides. The full set of attributes and operations are described below. .. describe:: profile == other Return :const:`True` if `profile` and `other` are equal. .. describe:: profile != other Return :const:`True` if `profile` and `other` are unequal. .. describe:: hash(profile) Return a hash of `profile` based on its contained values. .. describe:: profile[key] Return a profile value using a string key. Key names are identical to the corresponding attribute names. .. describe:: len(profile) Return the number of values encoded within this profile. .. describe:: dict(profile) Coerce the profile into a dictionary of key-value pairs. """ _keys = ("secure", "verify", "scheme", "user", "password", "address", "auth", "host", "port", "port_number", "protocol", "uri") _hash_keys = ("protocol", "secure", "verify", "user", "password", "address") def __init__(self, profile=None, **settings): # TODO: recognise IPv6 addresses explicitly self.__protocol = DEFAULT_PROTOCOL self.__secure = DEFAULT_SECURE self.__verify = DEFAULT_VERIFY self.__user = DEFAULT_USER self.__password = DEFAULT_PASSWORD self.__address = Address.parse("") self._apply_env_vars() if profile is None: pass elif isinstance(profile, string_types): self._apply_uri(profile) elif isinstance(profile, self.__class__): self._apply_settings(**{k: profile[k] for k in self._hash_keys}) elif isinstance(profile, Mapping): self._apply_settings(**profile) else: raise TypeError("Profile %r is neither a ConnectionProfile " "nor a string URI" % profile) self._apply_settings(**settings) if not self.address.port: addr = list(self.address) if self.protocol == "http": addr[1] = DEFAULT_HTTPS_PORT if self.secure else DEFAULT_HTTP_PORT else: addr[1] = DEFAULT_BOLT_PORT self.__address = Address(addr) def __repr__(self): return "%s(%r)" % (self.__class__.__name__, self.uri) def __str__(self): return {}»".format(self.uri) def __getitem__(self, key): if key in self._keys: return getattr(self, key) else: raise KeyError(key) def __len__(self): return len(self._keys) def __iter__(self): return iter(self._keys) def _apply_env_vars(self): if NEO4J_URI: self._apply_uri(NEO4J_URI) if NEO4J_AUTH: self._apply_settings(auth=NEO4J_AUTH) if NEO4J_SECURE: self._apply_settings(secure=(NEO4J_SECURE == "1")) if NEO4J_VERIFY: self._apply_settings(verify=(NEO4J_VERIFY == "1")) def _apply_uri(self, uri): settings = {} parsed = urlsplit(uri) if parsed.scheme is not None: self._apply_scheme(parsed.scheme) if "@" in parsed.netloc: settings["address"] = parsed.netloc.partition("@")[-1] else: settings["address"] = parsed.netloc if parsed.username: settings["user"] = parsed.username if parsed.password: settings["password"] = parsed.password self._apply_settings(**settings) def _apply_scheme(self, scheme): if scheme == "https": protocol, ext = "http", "s" else: protocol, _, ext = scheme.partition("+") if ext == "": self._apply_settings(protocol=protocol, secure=False, verify=True) elif ext == "s": self._apply_settings(protocol=protocol, secure=True, verify=True) elif ext == "ssc": self._apply_settings(protocol=protocol, secure=True, verify=False) else: raise ValueError("Unknown scheme extension %r" % ext) def _apply_settings(self, uri=None, scheme=None, protocol=None, secure=None, verify=None, address=None, host=None, port=None, port_number=None, auth=None, user=None, password=None, **other): if uri: self._apply_uri(uri) if scheme: self._apply_scheme(scheme) if protocol: self._apply_protocol(protocol) if secure is not None: self.__secure = secure if verify is not None: self.__verify = verify if isinstance(address, tuple): self.__address = Address(address) elif address: self.__address = Address.parse(address) if host and port: self.__address = Address.parse("%s:%s" % (host, port)) elif host: self.__address = Address.parse("%s:%s" % (host, self.port)) elif port: self.__address = Address.parse("%s:%s" % (self.host, port)) if isinstance(auth, tuple): self.__user, self.__password = auth elif auth: self.__user, _, self.__password = auth.partition(":") if user: self.__user = user if password: self.__password = password if other: raise ValueError("The following settings are not supported: %r" % other) def _apply_protocol(self, protocol): if protocol not in ("bolt", "http"): raise ValueError("Unknown protocol %r" % protocol) self.__protocol = protocol def __hash__(self): values = tuple(getattr(self, key) for key in self._hash_keys) return hash(values) def __eq__(self, other): self_values = tuple(getattr(self, key) for key in self._hash_keys) try: other_values = tuple(getattr(other, key) for key in self._hash_keys) except AttributeError: return False else: return self_values == other_values @property def secure(self): """ A flag for whether or not to apply security to the connection. If unspecified, and uninfluenced by environment variables, this will default to :const:`True`. """ return self.__secure @property def verify(self): """ A flag for verification of remote server certificates. If unspecified, and uninfluenced by environment variables, this will default to :const:`True`. """ return self.__verify @property def scheme(self): """ The URI scheme for contacting the remote server. If unspecified, and uninfluenced by environment variables, this will default to ``'bolt'``. """ if self.secure and self.verify: return "https" if self.protocol == "http" else self.protocol + "+s" elif self.secure: return self.protocol + "+ssc" else: return self.protocol @property def user(self): """ The user as whom to authorise. If unspecified, and uninfluenced by environment variables, this will default to ``'neo4j'``. """ return self.__user @property def password(self): """ The password which with to authorise. If unspecified, and uninfluenced by environment variables, this will default to ``'password'``. """ return self.__password @property def address(self): """ The full socket :class:`.Address` of the remote server. If unspecified, and uninfluenced by environment variables, this will default to ``IPv4Address(('localhost', 7687))``. """ return self.__address @property def auth(self): """ A 2-tuple of `(user, password)` representing the combined auth details. If unspecified, and uninfluenced by environment variables, this will default to ``('neo4j', 'password')``. """ return self.user, self.password @property def host(self): """ The host name or IP address of the remote server. If unspecified, and uninfluenced by environment variables, this will default to ``'localhost'``. """ return self.address.host @property def port(self): """ The port to which to connect on the remote server. This will be the correct port for the given :attr:`.protocol`. If unspecified, and uninfluenced by environment variables, this will default to ``7687`` (for Bolt traffic). """ return self.address.port @property def port_number(self): """ A variant of :attr:`.port` guaranteed to be returned as a number. In some cases, the regular port value can be a string, this attempts to resolve or convert that value into a number. If unspecified, and uninfluenced by environment variables, this will default to ``7687`` (for Bolt traffic). """ return self.address.port_number @property def protocol(self): """ The name of the underlying point-to-point protocol, derived from the URI scheme. This will either be ``'bolt'`` or ``'http'``, regardless of security and verification settings. If unspecified, and uninfluenced by environment variables, this will default to ``'bolt'``. """ return self.__protocol @property def uri(self): """ A full URI for the profile. This generally includes all other information, excluding auth details (for security reasons). If unspecified, and uninfluenced by environment variables, this will default to ``'bolt://localhost:7687'``. """ return "%s://%s:%s" % (self.scheme, self.host, self.port) @classmethod def from_config_parser(cls, parser, section, prefix=None): prefix = str(prefix or "") settings = {} for name, value in parser.items(section): if name.startswith(prefix): settings[name[len(prefix):]] = value uri = settings.pop("uri", None) return cls(uri, **settings)
[docs] @classmethod def from_file(cls, filenames, section, prefix=None): """ Load profile information from a configuration file. The required file format is described in the standard library ``configparser`` module, and is similar to that used in Windows INI files. :param filenames: :param section: :param prefix: :returns: :class:`ConnectionProfile` object created from the loaded configuration """ from py2neo.compat import ConfigParser parser = ConfigParser() parser.read(filenames) return cls.from_config_parser(parser, section, prefix)
[docs] def to_dict(self, include_password=False): """ Convert this profile to a dictionary, optionally including password information. :param include_password: if True then include the password in the return value, otherwise omit this information (default) """ if include_password: return dict(self) else: return {key: value for key, value in self.items() if key not in ("auth", "password")}
[docs] class ServiceProfile(ConnectionProfile): """ Connection details for a full Neo4j service, such as a cluster or single instance. This class extends :class:`.ConnectionProfile` so also inherits all of its attributes. """ _keys = ConnectionProfile._keys + ("routing",) _hash_keys = ConnectionProfile._hash_keys + ("routing",) def __init__(self, profile=None, **settings): self.__routing = False super(ServiceProfile, self).__init__(profile, **settings) @property def scheme(self): if self.protocol == "bolt" and self.routing: protocol = "neo4j" else: protocol = self.protocol if self.secure and self.verify: return "https" if protocol == "http" else protocol + "+s" elif self.secure: return protocol + "+ssc" else: return protocol @property def routing(self): """ Routing flag """ return self.__routing def _apply_protocol(self, protocol): if protocol == "neo4j": self.__routing = True super(ServiceProfile, self)._apply_protocol("bolt") else: super(ServiceProfile, self)._apply_protocol(protocol) def _apply_settings(self, uri=None, scheme=None, protocol=None, secure=None, verify=None, address=None, host=None, port=None, port_number=None, auth=None, user=None, password=None, **other): try: self.__routing = other.pop("routing") except KeyError: pass return super(ServiceProfile, self)._apply_settings(uri, scheme, protocol, secure, verify, address, host, port, port_number, auth, user, password, **other)