Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add explicit sync and asyncio APIs #40

Merged
merged 13 commits into from
Apr 30, 2023
38 changes: 38 additions & 0 deletions docs/asyncio.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Asyncio

`kr8s` uses `asyncio` under the hood when interacting with the Kubernetes API. However, it exposes a standard synchronous API by default.

```python
import kr8s

api = kr8s.api()
pods = api.get("pods")
```

For users that want it the `asyncio` API is also available via `kr8s.asyncio`.

```python
import kr8s.asyncio

api = kr8s.asyncio.api()
pods = await api.get("pods")
```

Submodules including `kr8s.objects` and `kr8s.portforward` also have `asyncio` equivalents at `kr8s.asyncio.objects` and `kr8s.asyncio.portforward`.

```python
from kr8s.asyncio.object import Pod

pod = Pod({
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "my-pod",
},
"spec": {
"containers": [{"name": "pause", "image": "gcr.io/google_containers/pause",}]
},
})

await pod.create()
```
16 changes: 10 additions & 6 deletions docs/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import kr8s

api = kr8s.api()

version = await api.version()
version = api.version()
print(version)
```

Expand All @@ -21,7 +21,7 @@ The client API is inspired by `kubectl` rather than the Kubernetes API directly
import kr8s

api = kr8s.api()
pods = await api.get("pods", namespace=kr8s.ALL)
pods = api.get("pods", namespace=kr8s.ALL)

for pod in pods:
print(pod.name)
Expand All @@ -33,12 +33,16 @@ For situations where there may not be an appropriate method to call or you want

To make API requests for resources more convenience `call_api` allows building the url via various kwargs.

```{note}
Note that `call_api` is only available via the [asyncio API](asyncio).
```

For example to get all pods you could make the following low-level call.

```python
import kr8s
import kr8s.asyncio

api = kr8s.api()
api = kr8s.asyncio.api()
async with api.call_api("GET", url="pods", namespace="") as r:
pods_response = await r.json()

Expand Down Expand Up @@ -87,7 +91,7 @@ api2 = kr8s.api()
```python
from kr8s.objects import Pod

pod = await Pod.get("some-pod")
pod = Pod.get("some-pod")
# pod.api is a pointer to api despite not being passed a reference due to caching
```

Expand All @@ -107,7 +111,7 @@ api2 = kr8s.Api(bypass_factory=True)
```python
from kr8s.objects import Pod

pod = await Pod.get("some-pod", api=api2)
pod = Pod.get("some-pod", api=api2)
# be sure to pass a reference around as caching will no longer work
```

Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"show-module-summary",
"imported-members",
]
autoapi_ignore = ["*tests*", "*conftest*"]
autoapi_ignore = ["*tests*", "*conftest*", "*asyncio*"]
# autoapi_python_use_implicit_namespaces = True
autoapi_keep_files = True
# autoapi_generate_api_docs = False
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ $ pip install kr8s
import kr8s

api = kr8s.api()
pods = await api.get("pods")
pods = api.get("pods")
```

### Object API
Expand All @@ -44,7 +44,7 @@ pod = Pod({
},
})

await pod.create()
pod.create()
```


Expand All @@ -62,6 +62,7 @@ installation
authentication
client
object
asyncio
```

```{toctree}
Expand Down
20 changes: 10 additions & 10 deletions docs/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Responses from the Client API are usually objects from [](#kr8s.objects) which r
import kr8s

api = kr8s.api()
pods = await api.get("pods", namespace=kr8s.ALL)
pods = api.get("pods", namespace=kr8s.ALL)
pod = pods[0]
print(type(pod))
# <class 'kr8s.objects.Pod'>
Expand Down Expand Up @@ -50,27 +50,27 @@ Objects also have helper methods for interacting with Kubernetes resources.

```python
# Patch the Pod
await pod.patch({"metadata": {"labels": {"foo": "bar"}}})
pod.patch({"metadata": {"labels": {"foo": "bar"}}})

# Check the Pod exists
await pod.exists()
pod.exists()
# True

# Update the object with the latest state from the API
await pod.refresh()
pod.refresh()

# Delete the Pod
await pod.delete()
pod.delete()
```

Some objects also have additional methods that are unique to them.

```python
# Get Pod logs
logs = await pod.logs()
logs = pod.logs()

# Check if Pod containers are ready
await pod.ready()
pod.ready()
# True
```

Expand All @@ -95,15 +95,15 @@ pod = Pod({
},
})

await pod.create()
pod.create()
```

Get a Pod reference by name.

```python
from kr8s.object import Pod

pod = await Pod.get("my-pod")
pod = Pod.get("my-pod")
```

When creating new objects they will not have a client reference because they are created directly. In this case the object will call the [](#kr8s.api) factory function which will either create a new client if none exists or will grab the first client from the cache if one was created somewhere else in your code.
Expand Down Expand Up @@ -192,7 +192,7 @@ class CustomObject(APIObject):

api = kr8s.api()

cos = await api.get("customobjects") # Will return a list of CustomObject instances
cos = api.get("customobjects") # Will return a list of CustomObject instances
```

```{note}
Expand Down
15 changes: 14 additions & 1 deletion kr8s/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2023, Dask Developers, Yuvi Panda, Anaconda Inc, NVIDIA
# SPDX-License-Identifier: BSD 3-Clause License
from ._api import Api, ALL, api # noqa
from functools import partial

from ._api import ALL # noqa
from ._api import Api as _AsyncApi
from ._asyncio import sync as _sync
from ._exceptions import NotFoundError # noqa
from .asyncio import api as _api # noqa

__version__ = "0.0.0"


@_sync
class Api(_AsyncApi):
__doc__ = _AsyncApi.__doc__


api = partial(_api, _asyncio=False)
59 changes: 36 additions & 23 deletions kr8s/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Api(object):

"""

_asyncio = True
_instances = weakref.WeakValueDictionary()

def __init__(self, **kwargs) -> None:
Expand Down Expand Up @@ -141,7 +142,7 @@ async def _get_kind(
watch: bool = False,
) -> dict:
"""Get a Kubernetes resource."""
from .objects import get_class
from ._objects import get_class

if not namespace:
namespace = self.auth.namespace
Expand Down Expand Up @@ -175,6 +176,22 @@ async def get(
field_selector: str = None,
) -> List[object]:
"""Get a Kubernetes resource."""
return await self._get(
kind,
*names,
namespace=namespace,
label_selector=label_selector,
field_selector=field_selector,
)

async def _get(
self,
kind: str,
*names: List[str],
namespace: str = None,
label_selector: str = None,
field_selector: str = None,
) -> List[object]:
async with self._get_kind(
kind,
namespace=namespace,
Expand All @@ -197,6 +214,24 @@ async def watch(
label_selector: str = None,
field_selector: str = None,
since: str = None,
):
"""Watch a Kubernetes resource."""
async for t, object in self._watch(
kind,
namespace=namespace,
label_selector=label_selector,
field_selector=field_selector,
since=since,
):
yield t, object

async def _watch(
self,
kind: str,
namespace: str = None,
label_selector: str = None,
field_selector: str = None,
since: str = None,
):
"""Watch a Kubernetes resource."""
async with self._get_kind(
Expand Down Expand Up @@ -251,25 +286,3 @@ def __version__(self):
from . import __version__

return f"kr8s/{__version__}"


def api(url=None, kubeconfig=None, serviceaccount=None, namespace=None) -> Api:
"""Create a :class:`kr8s.Api` object for interacting with the Kubernetes API.

If a kr8s object already exists with the same arguments, it will be returned.
"""

def _f(**kwargs):
key = frozenset(kwargs.items())
if key in Api._instances:
return Api._instances[key]
if all(k is None for k in kwargs.values()) and list(Api._instances.values()):
return list(Api._instances.values())[0]
return Api(**kwargs, bypass_factory=True)

return _f(
url=url,
kubeconfig=kubeconfig,
serviceaccount=serviceaccount,
namespace=namespace,
)
Loading