Skip to content

Commit cf545e7

Browse files
author
Chris Rossi
authored
NDB: Implement Client.context() (#7013)
As far as the public API is concerned, ``Client.context()`` replaces ``ndb_context``, which has been renamed to ``state_context`` and removed from the public API. ``_api.stub()`` has been refactored to store the Datastore stub in the current context.
1 parent 29fba13 commit cf545e7

File tree

13 files changed

+112
-67
lines changed

13 files changed

+112
-67
lines changed

ndb/docs/client.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
######
2+
Client
3+
######
4+
5+
.. automodule:: google.cloud.ndb.client
6+
:members:
7+
:show-inheritance:

ndb/docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@
207207
# Example configuration for intersphinx: refer to the Python standard library.
208208
intersphinx_mapping = {
209209
"python": ("https://docs.python.org/", None),
210+
"google-auth": ("https://google-auth.readthedocs.io/en/latest/", None),
210211
"google-cloud-datastore": (
211212
"https://googleapis.github.io/google-cloud-python/latest/",
212213
None,

ndb/docs/context.rst

Lines changed: 0 additions & 5 deletions
This file was deleted.

ndb/docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
:hidden:
77
:maxdepth: 2
88

9+
client
910
key
1011
model
1112
query
@@ -16,7 +17,6 @@
1617
blobstore
1718
metadata
1819
stats
19-
context
2020

2121
.. automodule:: google.cloud.ndb
2222
:no-members:

ndb/src/google/cloud/ndb/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@
6767
"ModelAdapter",
6868
"ModelAttribute",
6969
"ModelKey",
70-
"ndb_context",
7170
"non_transactional",
7271
"PickleProperty",
7372
"Property",
@@ -205,7 +204,6 @@
205204
from google.cloud.ndb.query import QueryIterator
206205
from google.cloud.ndb.query import QueryOptions
207206
from google.cloud.ndb.query import RepeatedStructuredPropertyPredicate
208-
from google.cloud.ndb._runstate import ndb_context
209207
from google.cloud.ndb.tasklets import add_flow_exception
210208
from google.cloud.ndb.tasklets import Future
211209
from google.cloud.ndb.tasklets import get_context

ndb/src/google/cloud/ndb/_api.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,30 @@
2020
from google.cloud import _http
2121
from google.cloud.datastore_v1.proto import datastore_pb2_grpc
2222

23+
from google.cloud.ndb import _runstate
2324

24-
def stub(client):
25-
"""Get a stub for the `Google Datastore` API.
2625

27-
Arguments:
28-
client (:class:`~client.Client`): An NDB client instance.
26+
def stub():
27+
"""Get the stub for the `Google Datastore` API.
28+
29+
Gets the stub from the current context, creating one if there isn't one
30+
already.
2931
3032
Returns:
3133
:class:`~google.cloud.datastore_v1.proto.datastore_pb2_grpc.DatastoreStub`:
3234
The stub instance.
3335
"""
34-
if client.secure:
35-
channel = _helpers.make_secure_channel(
36-
client._credentials, _http.DEFAULT_USER_AGENT, client.host
37-
)
38-
else:
39-
channel = grpc.insecure_channel(client.host)
40-
stub = datastore_pb2_grpc.DatastoreStub(channel)
41-
return stub
36+
state = _runstate.current()
37+
38+
if state.stub is None:
39+
client = state.client
40+
if client.secure:
41+
channel = _helpers.make_secure_channel(
42+
client._credentials, _http.DEFAULT_USER_AGENT, client.host
43+
)
44+
else:
45+
channel = grpc.insecure_channel(client.host)
46+
47+
state.stub = datastore_pb2_grpc.DatastoreStub(channel)
48+
49+
return state.stub

ndb/src/google/cloud/ndb/_runstate.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121

2222

2323
class State:
24-
eventloop = None
24+
def __init__(self, client):
25+
self.client = client
26+
self.eventloop = None
27+
self.stub = None
2528

2629

2730
class LocalStates(threading.local):
@@ -47,34 +50,13 @@ def current(self):
4750

4851

4952
@contextlib.contextmanager
50-
def ndb_context():
53+
def state_context(client):
5154
"""Establish a context for a set of NDB calls.
5255
53-
This function provides a context manager which establishes the runtime
54-
state for using NDB.
55-
56-
For example:
57-
58-
.. code-block:: python
59-
60-
from google.cloud.ndb import ndb_context
61-
62-
with ndb_context():
63-
# Use NDB for some stuff
64-
pass
65-
66-
Use of a context is required--NDB can only be used inside a running
67-
context. The context is used to coordinate an event loop for asynchronous
68-
API calls, runtime caching policy, and other essential runtime state.
69-
70-
Code within an asynchronous context should be single threaded. Internally,
71-
a :class:`threading.local` instance is used to track the current event
72-
loop.
73-
74-
In a web application, it is recommended that a single context be used per
75-
HTTP request. This can typically be accomplished in a middleware layer.
56+
Called from :meth:`google.cloud.ndb.client.Client.context` which has more
57+
information.
7658
"""
77-
state = State()
59+
state = State(client)
7860
states.push(state)
7961
yield
8062

@@ -91,14 +73,14 @@ def current():
9173
"""Get the current context state.
9274
9375
This function should be called within a context established by
94-
:func:`~google.cloud.ndb.ndb_context`.
76+
:meth:`google.cloud.ndb.client.Client.context`.
9577
9678
Returns:
9779
State: The state for the current context.
9880
9981
Raises:
10082
.ContextError: If called outside of a context
101-
established by :func:`~google.cloud.ndb.ndb_context`.
83+
established by :meth:`google.cloud.ndb.client.Client.context`.
10284
"""
10385
state = states.current()
10486
if state:

ndb/src/google/cloud/ndb/client.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
from google.cloud import client as google_client
2222
from google.cloud.datastore_v1.gapic import datastore_client
2323

24-
DATASTORE_API_HOST = datastore_client.DatastoreClient.SERVICE_ADDRESS.rstrip(
25-
":443"
26-
)
24+
from google.cloud.ndb import _runstate
25+
26+
DATASTORE_API_HOST = datastore_client.DatastoreClient.SERVICE_ADDRESS.rsplit(
27+
":", 1
28+
)[0]
2729

2830

2931
def _get_gcd_project():
@@ -60,6 +62,9 @@ def _determine_default_project(project=None):
6062
class Client(google_client.ClientWithProject):
6163
"""An NDB client.
6264
65+
The NDB client must be created in order to use NDB, and any use of NDB must
66+
be within the context of a call to :meth:`context`.
67+
6368
Arguments:
6469
project (Optional[str]): The project to pass to proxied API methods. If
6570
not passed, falls back to the default inferred from the
@@ -73,15 +78,45 @@ class Client(google_client.ClientWithProject):
7378
SCOPE = ("https://www.googleapis.com/auth/datastore",)
7479
"""The scopes required for authenticating as a Cloud Datastore consumer."""
7580

76-
secure = True
77-
"""Whether to use a secure connection for API calls."""
78-
7981
def __init__(self, project=None, namespace=None, credentials=None):
8082
super(Client, self).__init__(project=project, credentials=credentials)
8183
self.namespace = namespace
8284
self.host = os.environ.get(
8385
environment_vars.GCD_HOST, DATASTORE_API_HOST
8486
)
87+
self.secure = True
88+
89+
def context(self):
90+
"""Establish a context for a set of NDB calls.
91+
92+
This method provides a context manager which establishes the runtime
93+
state for using NDB.
94+
95+
For example:
96+
97+
.. code-block:: python
98+
99+
from google.cloud import ndb
100+
101+
client = ndb.Client()
102+
with client.context():
103+
# Use NDB for some stuff
104+
pass
105+
106+
Use of a context is required--NDB can only be used inside a running
107+
context. The context is used to manage the connection to Google Cloud
108+
Datastore, an event loop for asynchronous API calls, runtime caching
109+
policy, and other essential runtime state.
110+
111+
Code within an asynchronous context should be single threaded.
112+
Internally, a :class:`threading.local` instance is used to track the
113+
current event loop.
114+
115+
In a web application, it is recommended that a single context be used
116+
per HTTP request. This can typically be accomplished in a middleware
117+
layer.
118+
"""
119+
return _runstate.state_context(self)
85120

86121
@property
87122
def _http(self):

ndb/src/google/cloud/ndb/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,13 @@ class ContextError(Error):
3838
"""Indicates an NDB call being made without a context.
3939
4040
Raised whenever an NDB call is made outside of a context
41-
established by :func:`~google.cloud.ndb.ndb_context`.
41+
established by :meth:`google.cloud.ndb.client.Client.context`.
4242
"""
4343

4444
def __init__(self):
4545
super(ContextError, self).__init__(
4646
"No currently running event loop. Asynchronous calls must be made "
47-
"in context established by google.cloud.ndb.ndb_context."
47+
"in context established by google.cloud.ndb.Client.context."
4848
)
4949

5050

ndb/tests/unit/test__api.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from google.cloud import _http
1818
from google.cloud.ndb import _api
19+
from google.cloud.ndb import _runstate
1920

2021

2122
class TestStub:
@@ -30,7 +31,9 @@ def test_secure_channel(datastore_pb2_grpc, _helpers):
3031
host="thehost",
3132
spec=("_credentials", "secure", "host"),
3233
)
33-
stub = _api.stub(client)
34+
with _runstate.state_context(client):
35+
stub = _api.stub()
36+
assert _api.stub() is stub # one stub per context
3437
assert stub is datastore_pb2_grpc.DatastoreStub.return_value
3538
datastore_pb2_grpc.DatastoreStub.assert_called_once_with(channel)
3639
_helpers.make_secure_channel.assert_called_once_with(
@@ -45,7 +48,8 @@ def test_insecure_channel(datastore_pb2_grpc, grpc):
4548
client = mock.Mock(
4649
secure=False, host="thehost", spec=("secure", "host")
4750
)
48-
stub = _api.stub(client)
51+
with _runstate.state_context(client):
52+
stub = _api.stub()
4953
assert stub is datastore_pb2_grpc.DatastoreStub.return_value
5054
datastore_pb2_grpc.DatastoreStub.assert_called_once_with(channel)
5155
grpc.insecure_channel.assert_called_once_with("thehost")

0 commit comments

Comments
 (0)