Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions nutkit/frontend/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ def single(self):
req = protocol.ResultSingle(self._result.id)
return self._driver.send_and_receive(req, allow_resolution=True)

def single_optional(self):
"""Try leniently to fetch exactly one record.

Return None, if there is no record.
Return the record, if there is at least one.
Warn, if there are more than one.
"""
req = protocol.ResultSingleOptional(self._result.id)
return self._driver.send_and_receive(req, allow_resolution=True)

def peek(self):
"""Return the next Record or NullRecord without consuming it."""
req = protocol.ResultPeek(self._result.id)
Expand Down
195 changes: 195 additions & 0 deletions nutkit/protocol/cypher.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,198 @@ def __eq__(self, other):

# More in line with other naming
CypherPath = Path


class CypherPoint:
def __init__(self, system, x, y, z=None):
self.system = system
self.x = x
self.y = y
self.z = z
if system not in ("cartesian", "wgs84"):
raise ValueError("Invalid system: {}".format(system))

def __str__(self):
if self.z is None:
return "CypherPoint(system={}, x={}, y={})".format(
self.system, self.x, self.y
)
return "CypherPoint(system={}, x={}, y={}, z={})".format(
self.system, self.x, self.y, self.z
)

def __repr__(self):
return "<{}(system={}, x={}, y={}, z={})>".format(
self.__class__.__name__, self.system, self.x, self.y, self.z
)

def __eq__(self, other):
if not isinstance(other, type(self)):
return False

return all(getattr(self, attr) == getattr(other, attr)
for attr in ("system", "x", "y", "z"))


class CypherDate:
def __init__(self, year, month, day):
self.year = int(year)
self.month = int(month)
self.day = int(day)
for v in ("year", "month", "day"):
if getattr(self, v) != locals()[v]:
raise ValueError("{} must be integer".format(v))

def __str__(self):
return "CypherDate(year={}, month={}, day={})".format(
self.year, self.month, self.day
)

def __repr__(self):
return "<{}(year={}, month={}, day={})>".format(
self.__class__.__name__, self.year, self.month, self.day
)

def __eq__(self, other):
if not isinstance(other, type(self)):
return False

return all(getattr(self, attr) == getattr(other, attr)
for attr in ("year", "month", "day"))


class CypherTime:
def __init__(self, hour, minute, second, nanosecond, utc_offset_s=None):
self.hour = int(hour)
self.minute = int(minute)
self.second = int(second)
self.nanosecond = int(nanosecond)
# seconds east of UTC or None for local time
self.utc_offset_s = utc_offset_s
if self.utc_offset_s is not None:
self.utc_offset_s = int(utc_offset_s)
for v in ("hour", "minute", "second", "nanosecond", "utc_offset_s"):
if getattr(self, v) != locals()[v]:
raise ValueError("{} must be integer".format(v))

def __str__(self):
return (
"CypherTime(hour={}, minute={}, second={}, nanosecond={}, "
"utc_offset_s={})".format(
self.hour, self.minute, self.second, self.nanosecond,
self.utc_offset_s
)
)

def __repr__(self):
return (
"<{}(hour={}, minute={}, second={}, nanosecond={}, "
"utc_offset_s={})>".format(
self.__class__.__name__, self.hour, self.minute, self.second,
self.nanosecond, self.utc_offset_s
)
)

def __eq__(self, other):
if not isinstance(other, type(self)):
return False

return all(getattr(self, attr) == getattr(other, attr)
for attr in ("hour", "minute", "second", "nanosecond",
"utc_offset_s"))


class CypherDateTime:
def __init__(self, year, month, day, hour, minute, second, nanosecond,
utc_offset_s=None, timezone_id=None):
# The date time is always wall clock time (with or without timezone)
# If timezone_id is given (e.g., "Europe/Stockholm"), utc_offset_s
# must also be provided to avoid ambiguity.
self.year = int(year)
self.month = int(month)
self.day = int(day)
self.hour = int(hour)
self.minute = int(minute)
self.second = int(second)
self.nanosecond = int(nanosecond)

self.utc_offset_s = utc_offset_s
if self.utc_offset_s is not None:
self.utc_offset_s = int(utc_offset_s)
self.timezone_id = timezone_id
if self.timezone_id is not None:
self.timezone_id = str(timezone_id)

for v in ("year", "month", "day", "hour", "minute", "second",
"nanosecond", "utc_offset_s"):
if getattr(self, v) != locals()[v]:
raise ValueError("{} must be integer".format(v))
if timezone_id is not None and utc_offset_s is None:
raise ValueError("utc_offset_s must be provided if timezone_id "
"is given")

def __str__(self):
return (
"CypherDateTime(year={}, month={}, day={}, hour={}, minute={}, "
"second={}, nanosecond={}, utc_offset_s={}, timezone_id={})"
.format(
self.year, self.month, self.day, self.hour, self.minute,
self.second, self.nanosecond, self.utc_offset_s,
self.timezone_id
)
)

def __repr__(self):
return (
"<{}(year={}, month={}, day={}, hour={}, minute={}, second={}, "
"nanosecond={}, utc_offset_s={}, timezone_id={})>"
.format(
self.__class__.__name__, self.year, self.month, self.day,
self.hour, self.minute, self.second, self.nanosecond,
self.utc_offset_s, self.timezone_id
)
)

def __eq__(self, other):
if not isinstance(other, type(self)):
return False

return all(getattr(self, attr) == getattr(other, attr)
for attr in ("year", "month", "day", "hour", "minute",
"second", "nanosecond", "utc_offset_s",
"timezone_id"))


class CypherDuration:
def __init__(self, months, days, seconds, nanoseconds):
self.months = int(months)
self.days = int(days)
seconds, nanoseconds = divmod(
seconds * 1000000000 + nanoseconds, 1000000000
)
self.seconds = int(seconds)
self.nanoseconds = int(nanoseconds)

for v in ("months", "days", "seconds", "nanoseconds"):
if getattr(self, v) != locals()[v]:
raise ValueError("{} must be integer".format(v))

def __str__(self):
return (
"CypherDuration(months={}, days={}, seconds={}, nanoseconds={})"
.format(self.months, self.days, self.seconds, self.nanoseconds)
)

def __repr__(self):
return (
"<{}(months={}, days={}, seconds={}, nanoseconds={})>"
.format(self.__class__.__name__, self.months, self.days,
self.seconds, self.nanoseconds)
)

def __eq__(self, other):
if not isinstance(other, type(self)):
return False

return all(getattr(self, attr) == getattr(other, attr)
for attr in ("months", "days", "seconds", "nanoseconds"))
14 changes: 12 additions & 2 deletions nutkit/protocol/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class Feature(Enum):
# The driver offers a method for checking if a connection to the remote
# server of cluster can be established.
API_DRIVER_VERIFY_CONNECTIVITY = "Feature:API:Driver.VerifyConnectivity"
# The driver supports connection liveness check.
API_LIVENESS_CHECK = "Feature:API:Liveness.Check"
# The driver offers a method for the result to return all records as a list
# or array. This method should exhaust the result.
API_RESULT_LIST = "Feature:API:Result.List"
Expand All @@ -32,8 +34,12 @@ class Feature(Enum):
# This method asserts that exactly one record in left in the result
# stream, else it will raise an exception.
API_RESULT_SINGLE = "Feature:API:Result.Single"
# The driver supports connection liveness check.
API_LIVENESS_CHECK = "Feature:API:Liveness.Check"
# The driver offers a method for the result to retrieve the next record in
# the result stream. If there are no more records left in the result, the
# driver will indicate so by returning None/null/nil/any other empty value.
# If there are more than records, the driver emits a warning.
# This method is supposed to always exhaust the result stream.
API_RESULT_SINGLE_OPTIONAL = "Feature:API:Result.SingleOptional"
# The driver implements explicit configuration options for SSL.
# - enable / disable SSL
# - verify signature against system store / custom cert / not at all
Expand All @@ -43,6 +49,10 @@ class Feature(Enum):
# ...+s: enforce SSL + verify server's signature with system's trust store
# ...+ssc: enforce SSL but do not verify the server's signature at all
API_SSL_SCHEMES = "Feature:API:SSLSchemes"
# The driver supports sending and receiving geospatial data types.
API_TYPE_SPATIAL = "Feature:API:Type.Spatial"
# The driver supports sending and receiving temporal data types.
API_TYPE_TEMPORAL = "Feature:API:Type.Temporal"
# The driver supports single-sign-on (SSO) by providing a bearer auth token
# API.
AUTH_BEARER = "Feature:Auth:Bearer"
Expand Down
38 changes: 36 additions & 2 deletions nutkit/protocol/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,34 @@ class StartTest:
"""
Request the backend to confirm to run a specific test.

The backend should respond with RunTest if the backend wants the test to be
skipped it must respond with SkipTest.
The backend should respond with RunTest unless it wants the test to be
skipped, in that case it must respond with SkipTest.

The backend might also respond with RunSubTests. In this case, TestKit will
- run the test, if it does not have subtests
- ask for each subtest whether it should be run, if it has subtests
StartSubTest will be sent to the backend, for each subtest
"""

def __init__(self, test_name):
self.testName = test_name


class StartSubTest:
"""
Request the backend to confirm to run a specific subtest.

See StartTest for when TestKit might emmit this message.

The backend should respond with RunTest unless it wants the subtest to be
skipped, in that case it must respond with SkipTest.
"""

def __init__(self, test_name, subtest_arguments: dict):
self.testName = test_name
self.subtestArguments = subtest_arguments


class GetFeatures:
"""
Request the backend to the list of features supported by the driver.
Expand Down Expand Up @@ -394,6 +414,20 @@ def __init__(self, resultId):
self.resultId = resultId


class ResultSingleOptional:
"""
Request to expect and return exactly one record in the result stream.

Furthermore, the method is supposed to fully exhaust the result stream.

The backend should respond with a RecordOptional or, if any error occurs
while retrieving the records, an Error response should be returned.
"""

def __init__(self, resultId):
self.resultId = resultId


class ResultPeek:
"""
Request to return the next result in the Stream without consuming it.
Expand Down
30 changes: 26 additions & 4 deletions nutkit/protocol/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
"""


class RunTest:
"""Response to StartTest indicating that the test can be started."""


class FeatureList:
"""
Response to GetFeatures.
Expand All @@ -37,6 +33,14 @@ def __init__(self, features=None):
self.features = features


class RunTest:
"""Response to StartTest indicating that the test can be started."""


class RunSubTests:
"""Response to StartTest requesting to decide for each subtest."""


class SkipTest:
"""Response to StartTest indicating that the test should be skipped."""

Expand Down Expand Up @@ -215,6 +219,24 @@ def __init__(self, records=None):
self.records = record_list


class RecordOptional:
"""
Represents an optional record.

Possible response to the ResultOptionalSingle request.

Fields:
record: Record values or None (see Record response)
warnings: List of warnings (str) (potentially empty)
"""

def __init__(self, record, warnings):
self.record = None
if record is not None:
self.record = Record(values=record["values"])
self.warnings = warnings


class Summary:
"""Represents summary returned from a ResultConsume request."""

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ ifaddr~=0.1.7
lark~=1.0.0
nose~=1.3.7
pre-commit~=2.15.0
pytz
Empty file.
Loading