kmock package¶
- class kmock.AiohttpInterceptor(host, port=None, extra=None, own_resolver=None)[source]¶
Bases:
AsyncContextManager[AiohttpInterceptor,bool|None]Intercept hostname resolution for selected hostnames to specified IP:port.
The hostname filter can be specified either by explicit string or a regexp, or by a list/set of strings/regexpsts. The regexp patterns must match fully. String hostnames are case-insensitive, while patterns are case-sensitive (add
re.Ito make it case insensitive explicitly).For extra precision, the filter can be also specified as tuple(s) with the hostname (as defined above) and a specific port (integer).
Examples:
'kopf.dev're.compile(r'kopf..*')['kopf.dev', re.compile(r'kopf..*')]('kopf.dev', 80)(re.compile(r'kopf..*'), 443){('kopf.dev', 80), (re.compile(r'kopf..*'), 443), 'kopf.cloud'}
If the target port is not specified, it resolves to the requested port. Otherwise, it is also intercepted and forced to the specified port.
The filter can be modified at runtime if needed.
The target host & port are always intercepted regardless of the filter.
The resolver affects the hostname resolution only since the moment it is entered and only until it is exited. Multiple resolvers must be stacked as LIFO (last-in-first-out): i.e. exited in the reverse sequence as entered.
- Parameters:
- class kmock.Selectable(*args, **kwargs)[source]¶
Bases:
ProtocolA minimally sufficient resource to be recognized for selection/filtering. Anything less that that is not even considered. More fields CAN be present (as per other protocols), but not required.
- class kmock.Request(*, id=0, method=None, url=URL(''), params=NOTHING, headers=NOTHING, cookies=NOTHING, body=b'', text='', data=None, action=None, resource=None, namespace=None, name=None, subresource=None, raw_request=None)[source]¶
Bases:
objectAn incoming request with pre-parsed HTTP- & Kubernetes-specific intentions.
The request can be compared/asserted against a wide range of simpler types if they are understood by
kmock.Criteriaand descendants:assert kmock.gets[0] == b'{}' # bytes are request bodies assert kmock.gets[0] == {'status': {}} # dicts do partial nested json matching assert kmock.gets[0] == '/api/v1' # strings starting with slash are full paths assert kmock.gets[0] == re.compile(r'/api/v1.*') # regexps are paths assert kmock.gets[0] == 'get' # known HTTP methods are directly supported assert kmock.gets[0] == 'list' # known Kubernetes actions are directly supported assert kmock.gets[0] == kmock.method.POST # so as enums assert kmock.gets[0] == kmock.action.WATCH # so as enums assert kmock.gets[0] == kmock.resource('', 'v1', 'pods') # Kubernetes specific resources assert kmock.gets[0] == kmock.resource(group='kopf.dev') # Kubernetes partial resources assert kmock.gets[0] == 'ns' # BEWARE: strings are namespaces, not the requested content
If implicit comparision is not sufficient, specific fields can be used. It is more verbose and wordy but very precise.
Note
intis not supported for HTTP statuses: requests have no statuses, those are in responses. We do not assert on responses since the user typically defines the responses manually.
- class kmock.Criteria[source]¶
Bases:
objectA standalone container for specialized named criteria.
- static guess(arg, /)[source]¶
- Parameters:
arg (Criteria | EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, Criterion] | set[Criterion] | frozenset[Criterion] | tuple[Criterion, ...] | Event | Event | Future[Criterion] | Future[Criterion] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool]] | set[EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, Criterion] | set[Criterion] | frozenset[Criterion] | tuple[Criterion, ...] | Event | Event | Future[Criterion] | Future[Criterion] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool]] | frozenset[EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, Criterion] | set[Criterion] | frozenset[Criterion] | tuple[Criterion, ...] | Event | Event | Future[Criterion] | Future[Criterion] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool]] | tuple[EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, Criterion] | set[Criterion] | frozenset[Criterion] | tuple[Criterion, ...] | Event | Event | Future[Criterion] | Future[Criterion] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool], ...] | Event | Event | Future[EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, Criterion] | set[Criterion] | frozenset[Criterion] | tuple[Criterion, ...] | Event | Event | Future[Criterion] | Future[Criterion] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool]] | Future[EllipsisType | None | str | bool | bytes | SupportsBool | Pattern[str] | Pattern[bytes] | action | method | resource | Selectable | Mapping[str, Criterion] | set[Criterion] | frozenset[Criterion] | tuple[Criterion, ...] | Event | Event | Future[Criterion] | Future[Criterion] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool]] | Callable[[], bool] | Callable[[], SupportsBool] | Callable[[Request], bool] | Callable[[Request], SupportsBool] | data | text[Pattern[str]] | body[Pattern[bytes]] | params[Pattern[str]] | headers[Pattern[str]] | cookies[Pattern[str]])
- Return type:
Criteria | None
- class kmock.RawHandler(stream_queue=NOTHING, stream_task=None, *, limit=None, strict=False, entered=0)[source]¶
Bases:
RootA mock handler to be injected into WSGi-like servers.
The criteria & responses can be injected with the
<</>>(see Requests matching and Prepared responses).For a locally running server with a TCP port, see
Server.Implementation details:
Due to the nature of async/await routines, it is impossible to notify the stream consumers about the addition of new items from synchronous methods like
<</>>— it is only possible from asynchronous ones.The only known primitive supporting synchronous “nowait” is
asyncio.Queue: even if the item is put viaasyncio.Queue.put_nowait, all async consumers get it immediately.To work around this limitation, the handler/server MUST be “entered” as a context manager (
async with), which implicitly starts a background task and uses a queue to deliver the items in the “nowait” mode.This comes with trade-offs:
First, there can be a minor delay after stream feeding and before the items are delivered to live streams. This is actually utilised to pack several fed items into one batch, as in:
kmock[...] << b'hello' << b'world' << ....Second, if the code has no
awaitsomewhere after feeding the content, the streams can be blocked with no actual delivery happening until the code gives control back to the event loop via the nextawait.
- Parameters:
- property errors: list[Exception][source]¶
Errors of the current level of context managers (for assertions).
- class kmock.Server(handler, host='127.0.0.1', port=None, server_cls=<class 'aiohttp.test_utils.RawTestServer'>, client_fct=<class 'aiohttp.client.ClientSession'>, hostnames=None, *, user_agent='kmock/0.7.dev7+g7e5aaad4d aiohttp/3.13.5')[source]¶
Bases:
objectAll-in-one bundle needed to run the mock app on an HTTP port.
The handlers are simple WSGI-like apps that have no networking capabilities. The servers implement networking, specifically listening on a TCP/HTTP port and marshalling the requests & responses to/from the handler.
Kmock uses
aiohttpby default. Using other libraries is possible as long as the incoming reqeusts & responses support the core signatures of aiohttp. This is NOT an officially supported functionality and might break any time.There can be several severs pointing to the same handler — e.g. with dfferent hosts:ports, therefore different URLs. The traffic is balanced across such entry points when the client methods of the handler are used. When the client methods of a specific server are used, the traffic goes through that server’s endpoint only.
A few examples with 2 servers on different random ports pointing to the same handler, which defines the reactions & accumulates the requests.
Automatically balanced traffic:
async with RawHandler() as kmock, Server(kmock), Server(kmock): for _ in range(10): await kmock.get('/')
Routing the traffic manually by directly using the server:
async with RawHandler() as kmock, Server(kmock), Server(kmock) as srv2: for _ in range(10): await srv2.client.get('/')
The same as above, but via the handler (e.g. via a fixture):
async with RawHandler() as kmock, Server(kmock), Server(kmock): for _ in range(10): await kmock.clients[1].get('/')
- Parameters:
- handler: RawHandler[source]¶
- hostnames: Pattern[str] | str | tuple[Pattern[str] | str, int | None] | Collection[Pattern[str] | str | tuple[Pattern[str] | str, int | None]] | None[source]¶
- property host: str[source]¶
The real host (IP address) assigned to the server (when started).
If listening on a catch-all host (e.g. 0.0.0.0), this will report any IP address of the current machine, preferably localhost/127.0.0.1. I.e., it points to the host where the server can be accessed, not only where it is listening.
- property server: BaseTestServer[source]¶
- class kmock.KubernetesScaffold(stream_queue=NOTHING, stream_task=None, *, limit=None, strict=False, entered=0)[source]¶
Bases:
RawHandlerA bare structure of the Kubernetes API: errors, API & resource discovery.
It is stateless! It keeps nothing in memory except for what was fed into it. For the stateful API, see
KubernetesEmulator.- Parameters:
- property resources: ResourcesArray[source]¶
- class kmock.KubernetesEmulator(stream_queue=NOTHING, stream_task=None, *, limit=None, strict=False, entered=0, buses=NOTHING)[source]¶
Bases:
KubernetesScaffoldA server that mimics Kubernetes and tracks the state of objects in memory.
Object creation, patching, and deletion are tracked. The operations emulate the behaviour of a realistic Kubernetes server, but very simplistically. The server then serves these objects on listings, watch-streams, fetching. It is essentially an in-memory-database-over-http.
However, unlike the real Kubernetes server, this emulator only modifies the objects as simple JSON structures: merge the dicts, overwrite the keys. There is no special treatment of “special” fields, e.g. lists where the new values are added/merged instead of overwriting the whole dict key. If you need “special” field treament, inherit and implement for your cases.
All objects are available for assertions via the
kmock.objectsfield (in addition to the inheritedkmock.requestsand others).Note: the object tracking happens even if there are reactions that catch the creation/update/deletion operations. However, the objects are shown only in the default handlers, i.e. when the fetching/listing/watching requests are not intercepted (because those responses will return their content instead of the stored states of the objects).
See also
Simulator vs. emulator: https://stackoverflow.com/a/1584701/857383
- Parameters:
- property objects: ObjectsArray[source]¶
- class kmock.SinkBox(arg, /)[source]¶
Bases:
object- Parameters:
arg (None | Path | RawIOBase | TextIOBase | BufferedIOBase | MutableSequence[Request] | MutableSet[Request] | Future[Request] | Future[Request] | Queue[Request] | Queue[Request] | Event | Condition | Event | Condition | Bus[Request] | Generator[Sink | Any, Request | None, Sink | None] | AsyncGenerator[Sink | Any, Request | None] | Awaitable[Sink | Any] | Callable[[], Sink | Any] | Callable[[Request], Sink | Any] | SinkBox)
- class kmock.action(*values)[source]¶
-
Which K8s action is requested, based on the HTTP method & URL structure.
Note: It follows the same convention of using verbs as in HTTP
method, except for the deletion: it would conflict with the well-known HTTP verb. In most cases, there is no big difference between the HTTP methodDELETEand the Kubernetes-level action of deletion, so the"delete"can be used. If it matters, the action criterion can be enforced either by passing an enum valueaction.DELETEexplicitly (not a string), or as the keyword argumentaction='delete'(both name & value are accepted in this case).
- class kmock.data(arg: Any, /)[source]¶
- class kmock.data(*args: Mapping[Any, Any], **kwargs: Any)
Bases:
object
- class kmock.resource(arg1=None, arg2=None, arg3=None, /, *, group=None, version=None, plural=None)[source]¶
Bases:
SelectableA resource specification that can match several resource kinds.
The resource specifications are not usable in K8s API calls, as the API has no endpoints with masks or placeholders for unknown or catch-all resource identifying parts (e.g. any API group, any API version, any name).
They are used only locally in the operator to match against the actual resources with specific names (
kmock.resource). The handlers are defined with resource specifications, but are invoked with specific resource kinds. Even if those specifications look very concrete and allow no variations, they still remain specifications.- Parameters:
- check(resource)[source]¶
- Parameters:
resource (Selectable)
- Return type:
- class kmock.View[source]¶
Bases:
ABCA base view into collections of requests with all the filtering DSL in it.
- class kmock.Root(stream_queue=NOTHING, stream_task=None)[source]¶
Bases:
ViewA view for the root handlers/servers where the requests first arrive.
It is needed as the terminal point of walking the tree of views for registering the payloads on
<<&>>operators. Also as a shared sync-to-async live feeder (at least one root must be “entered”).
- class kmock.OrGroup(sources)[source]¶
Bases:
GroupThe OR-group to check that a request belongs to ANY of the grouped filters.
Examples:
(kmock['get'] | kmock['/'] | kmock['?q=query']) << b'hello', except that not all filters can be put in a set (e.g. dicts).When it comes to iterating or indexing the requests, they are concatenated in the order of grouping, i.e.
a|b|care yielded in this order: a, b, c. As such,(a|b|c)[0]or(a|b|c)[-1]can belong to either group depending on what is seen (recorded & filtered) in the other groups.
- class kmock.AndGroup(sources)[source]¶
Bases:
GroupThe AND-group to check that a request belongs to ALL of the grouper filters.
Examples:
kmock['get'] & kmock['/']. The same askmock['get', '/'].Grouping views to different roots (handlers/servers) makes no sense, as it will always be empty by definition (a request can come only from one root).
- class kmock.Chained(source)[source]¶
Bases:
ViewA base view for all other views with a single source, forming a _chain_.
- Parameters:
source (View)
- class kmock.Exclusion(source, exclusions)[source]¶
Bases:
ChainedA NOT-filter for requests in the source EXCEPT requests in excluded sources.
Examples:
kmock['get'] - kmock['/'] - kmock['?q=query'].
- class kmock.Slicer(source, s)[source]¶
Bases:
ChainedA slice of requests, as in list: with the
[start:stop:step]notation.Examples:
kmock['get'][1:],kmock['post'][:3],kmock['/'][::2].
- class kmock.Filter(source, criteria)[source]¶
Bases:
ChainedA criteria-based filter or filter by arbitrary bool-evaluable callback.
Examples:
kmock['get']['/'][{'watch': 'true'}]kmock[{'Content-Type': 'application/json'}]kmock[lambda req: req.params.get('idx') == '5']kmock['post', kmock.body(re.compile(r'.*hello.*'))]
A sequence of filters is collapsed into one filter preserving the logic. If there are apparently conflicting criteria, which lead to no result, an errors is raised: e.g.
kmock['get']['post'].
- class kmock.Priority(source, priority)[source]¶
Bases:
ChainedAn internal hint to pass the info through the chain.
It is consumed when ordering the payloads to be served on each request.
The chain can stretch from the first root node till the final payload node, e.g.:
kmock.fallback['get']['/'][:1] << b''; or directly precede the payload:kmock['get']['/'][:1].fallback << b''.An alternative would be to store the priority attribute on every node, even on those that have no use or need for prioritizing (bad abstractions).
A hint: use
(… ** a) ** bfor sub-priority A for the same values of B. But beware the Python math:a**b**cisa**(b**c), not(a**b)**c. E.g.(kmock ** -100) ** -math.inf << 404for the sub-fallback.
- class kmock.Reaction(source, response=NOTHING)[source]¶
Bases:
ChainedAn actionable response reaction for a filter with payloads and side effects.
Examples:
kmock['get /'] << b'hello'kmock['get /'] >> callback_fn >> (reqs:=[])kmock['get /'] << 301 << 'Location: https://kopf.dev' << b'Use Kopf!'kmock.fallback << 404
Worth noting: creating a new reaction from the old reaction (by using chained
<<&>>operators) does not simply add the new reaction in addition to the old one, but replaces it to avoid duplicates.As such, in this code,
r1will never serve or record any requests, onlyr2will (both payload items will be served byr2as a stream). Essentially,r1becomes abandoned & deactivated at creation ofr2:r1 = kmock['get /'] << b'hello, ' r2 = r1 << b'world!\n'
This is an intentional design decision. Otherwise, i.e. by keeping
r1active, it might lead to iconsistencies: eitherr1will record requests that it did not actually serve, orr1will serve regular (non-streamed) payloads followed by streamingr2, which is a conflicting behaviour.For extra code safety, assertions on deactivated reactions are prohibited. If there is a scenaro where this is needed, please report it as an issue.
- Parameters:
source (View)
response (Response)
- class kmock.Stream(source, batch=NOTHING)[source]¶
Bases:
ChainedA pseudo-view into “live” (aka “lazy”) responses or streams.
Examples:
kmock['/'] << ...; loop.call_later(9, lambda: kmock['get /'][...] << b'hello')kmock['/'] << (...,); loop.call_later(9, lambda: kmock[...] << b'all streams see' << ...)
- Parameters:
source (View)
batch (StreamBatch)
- class kmock.ObjectVersion(value=None, **kwargs)[source]¶
Bases:
MutableMapping[str,Any]A single-dict wrapper with extra syntax features for simpler/shorter tests.
In the core, it is a read-only mapping with a view into the wrapped dict, typically with the keys like
metadata,spec, andstatus, but this is neither required nor guaranteed.Unlike the regular dict, the object has set-like recursive partial matching. However, both the keys presence and their values must match on comparison.
The actual “greater” object may contain more keys than the “smaller” pattern expects (typically, the metadata, names/namespaces, or resource versions), but those in the pattern are required and must store the expected values.
The inverse comparison —the “smaller” raw dict & the “greater” dict view— is NOT a boolean inverse of the straighforward operator, but a role switch: the “smaller” unwrapped raw dict becomes a pattern, the “greater” wrapped dict becomes an object to be checked with extra keys.
There are, by design, no
>or<operators, only>=and<=. The strict inclusion-except-equality of dicts makes no sense semantically. Also to avoid confusion between the role switching and inverse comparison.All in all, the actual pattern is always on the “smaller” side (always!).
Example with the object containing the pattern (and maybe more):
assert kmock.objects[r, 'ns', 'n'] >= {'spec': {'key': 'must be this'}} assert {'spec': {'key': 'must be this'}} <= kmock.objects[r, 'ns', 'n']
Example with the object NOT containing the pattern (can contain more keys):
assert not kmock.objects[r, 'ns', 'n'] >= {'spec': {'key': 'not this'}} assert not {'spec': {'key': 'not this'}} <= kmock.objects[r, 'ns', 'n']
The
...akaEllipsismeans “any value but the key must be present” (or “must be absent” with the negation/inversion):assert kmock.objects[r, 'ns', 'n'] >= {'spec': {'key': ...}} # present assert not kmock.objects[r, 'ns', 'n'] >= {'spec': {'key': ...}} # absent
Similar to regular dicts, the equality compares the dict as a whole, i.e. no extra keys are allowed in the object (
...is supported though):assert kmock.objects[r, 'ns', 'n'] == { 'metadata': {'name': 'n1', 'namespace': ...}, 'spec': 'must be this', }
The inequality means that the values mismatch or there are the extra keys:
assert kmock.objects[r, 'ns', 'n'] != {'spec': {'key': ...}}
For pattern matching of two raw dicts, e.g. from an API, wrap one & compare to a plain-dict pattern, or wrap the plain-dict pattern itself, just to bring the magic to play (but mind the direction of the comparison):
assert kmock.PatternDict(resp['items'][0]) >= {'spec': {'key': ...}} assert resp['items'][0] >= kmock.PatternDict({'spec': {'key': ...}}) assert {'spec': {'key': ...}} <= kmock.PatternDict(resp['items'][0]) assert kmock.PatternDict({'spec': {'key': ...}}) <= resp['items'][0]
Nested dicts (and only dicts/mappings) are enhanced with magic on access:
assert kmock.objects[r, 'ns', 'n']['spec'] >= {'key': ...}
But the nested non-dicts, i.e. regular values, lists, tuples, are not:
assert kmock.objects[r, 'ns', 'n']['spec']['key'] == 'val' # no "..."!
To access a raw dict —nested or top-level— convert it to
dict(). Then Python does the comparison, i.e. with no recursive partial matching:assert dict(kmock.objects[r, 'ns', 'n'])['spec'] == {'key': 'only this'} assert dict(kmock.objects[r, 'ns', 'n']['spec'])['key'] == 'only this'
Note: it is challenging to reliably wrap the nested non-dict values and expose their specialized fields/methods, behaviour, math, reprs, etc. Especially in identity checks like
obj['key'] is None. The...magic is not supported on the extracted scalars or lists.
- class kmock.ObjectHistory(items=(), /)[source]¶
Bases:
MutableSequence[ObjectVersion|None]Addressable and adjustable history of dict versions.
Noneis used as a soft-deletion marker.- pop([index]) item -- remove and return item at index (default last).[source]¶
Raise IndexError if list is empty or index is out of range.
- Parameters:
index (int)
- Return type:
ObjectVersion | None
- remove(value, /)[source]¶
S.remove(value) – remove first occurrence of value. Raise ValueError if the value is not present.
- property last: ObjectVersion[source]¶
The last seen state before the soft-deletion(s) or after the patch(es).
- property raw: list[ObjectVersion | None][source]¶
- class kmock.Object(history=(), /)[source]¶
Bases:
MutableMapping[str,Any]A view of a single object:
kmock.objects[resource, namespace, name].==&!=recursively compare for precise equality.<=&>=recursively compare against a partial pattern.
Accessing/modifying the versioned dict delegates to the latest version of the dict. The previous versions are unaffected unless modified directly:
kmock.objects['v1/pods', 'ns', 'n'].history[0]['spec'] = {'key': 'val'}
The modification of individual keys or keys en mass, such as
.update(…), changes the latest version and does not grow the history — in order to make it easier to modify objects in tests without the API-level side effects. Only the dedicated methods for creating/patching/deleting grow the history.Another critical difference of
.patch()vs..update():.patch()merges the sub-dicts as it happens in the Kubernetes API;.update()does not merge the sub-dicts, instead it overwrites them as if simply assigned by keys — to mimic the behavior of Python’s dicts. This method is is a part of theMutableMapping, not an API-like method:obj = VersionedDict({'spec': {'interval': 10, 'timeout': 60}}) obj.patch({'spec': {'interval': 20}}) assert obj == {'spec': {'interval': 20, 'timeout': 60}} obj.update({'spec': {'interval': 30}}) assert obj == {'spec': {'interval': 30}} # Synonymous to: obj['spec'] = {'interval': 30}
If there is no latest version —whether because it was never populated or because it was soft-deleted— then a new empty dict is auto-populated.
- clear()[source]¶
Clear the data from the latest visible version (not the history!).
- Return type:
None
- delete()[source]¶
Soft-delete the object by marking it as such but retaining the history.
It is possible to soft-delete the versioned dict a few times in a row.
In contrast,
del kmock.objects[…]physically deletes it from memory, whilekmock.objects[…].delete()keeps the object’s history. The API emulator uses the soft-deletion internally.- Return type:
None
- create(changes=None, /, **kwargs)[source]¶
Populate the dict with the new data.
Can be called only if the object is either empty or soft-deleted.
- patch(changes=None, /, **kwargs)[source]¶
Recursively patch the object by creating a new version.
For scalar values, it is the same as native Python update (replacing). For dicts, it is a recursive merge (patching). This is so to prevent a dict value in the patch to replace the whole pre-existing dict in the base object instead of several keys in it:
obj = Object({'spec': {'timeout': 60, 'interval': 10}}) obj.patch({'spec': {'interval': 15}}) assert obj == {'spec': {'timeout': 60, 'interval': 15}}
Note the preservance of
timeout=60in this example, which would be lost with the native Python update.Noneremoves the key (even if nested).
- property last: ObjectVersion[source]¶
The last seen state before the soft-deletion(s) or after the patch(es).
- property history: ObjectHistory[source]¶
The full history of the dict, all its versions & soft-deletion markers.
- class kmock.ObjectsArray[source]¶
Bases:
objectAn associative array of K8s objects and their past versions.
The following notations are supported with a 3-item key:
kmock.objects['v1/pods', 'ns', 'name'] # namespaced objects kmock.objects['v1/pods', None, 'name'] # cluster-wide objects
The 4-item key is a shortcut for the
.historyfield of the object view:kmock.objects['v1/pods', 'ns', 'name', 0] # the initial version kmock.objects['v1/pods', 'ns', 'name', -2] # the pre-last version kmock.objects['v1/pods', 'ns', 'name', -1] # the current version kmock.objects['v1/pods', 'ns', 'name', 1:3] # a slice of versions kmock.objects['v1/pods', 'ns', 'name', :] # a list of all versions
In both cases, the first item of the key is either a pre-created instance of
kmock.resource, or a string or an object that can be passed to and parsed bykmock.resource: e.g.,'kopf.dev/v1/kopfexamples','kopfexamples.v1.kopf.dev','v1/pods','pods.v1', and some other notations; this includes all objects with string properties.group,.version,.plural.There are 2 ways to delete the object:
del kmock.objects['v1/pods', 'ns', 'n']hard-deletes it from memory.kmock.objects['v1/pods', 'ns', 'n'].delete()soft-deletes the object.
When the object is hard-deleted, a new object with the same name begins a new history starting with the version zero. The previous history is lost.
When the object is soft-deleted, a placeholder
Noneis put as the latest version. It does NOT compare to any dict anymore (always raises an error). However, it still has.historyto access the past states of the object, or.lastto access the last seen non-deleted state.The array does not do any introspection or interpretation of an object’s stored contents, such as name-, namespace-, or resource-guessing. These elements must be provided as the object’s address (key) explicitly. The API handler does the introspection from the URL and the request payload.
A new object with the same name continues the versioning with the new state. The following state of the object’s history is possible (note: the
<=&>=mean set-like “includes but may contain more”):POST /…/namespaces/ns/… ← {'spec': '1st', 'metadata': {'name': 'n1'}} PATCH /…/namespaces/ns/…/n1 ← {'spec': '1st modified'} DELETE /…/namespaces/ns/…/n1 POST /…/namespaces/ns/… ← {'spec': '2nd', 'metadata': {'name': 'n1'}} assert len(kmock.objects[r, 'ns', 'n1', 0].history) == 4 assert kmock.objects[r, 'ns', 'n1', 0] >= {'spec': '1st'} assert kmock.objects[r, 'ns', 'n1', 1] >= {'spec': '1st modified'} assert kmock.objects[r, 'ns', 'n1', 2] is None assert kmock.objects[r, 'ns', 'n1', 3] >= {'spec': '2nd'}Objects tend to be sorted chronologically in the order of first appearance with the same code flow — the same as it happens for usual Python dicts. The hard-deletion followed by re-creation moves the object to the end. The soft-deletion followed by re-creation keeps it in its original position.
- class kmock.ResourceInfo(*, namespaced=None, kind=None, singular=None, verbs=NOTHING, shortnames=NOTHING, categories=NOTHING, subresources=NOTHING)[source]¶
Bases:
objectThe extended discovery information about a resource.
The identifying fields —group, version, plural— are not part of the class, as they are used as the key of
ResourceArrayorkmock.resourcesthat leads to the extended resource information.- Parameters:
- class kmock.ResourcesArray(resources=None, /)[source]¶
Bases:
MutableMapping[str|Selectable|resource,ResourceInfo|ResourceDict]An associative array of extended information about cluster references.
Exposed via
kmock.resources.The key is either a pre-created instance of
kmock.resource, or a string that can be passed to and parsed bykmock.resource: e.g.,'kopf.dev/v1/kopfexamples','kopfexamples.v1.kopf.dev','v1/pods','pods.v1', and some other notations; this includes all objects with string properties.group,.version,.plural. It is stored pre-parsed, so iterating over the array yields the parsed instances ofkmock.resource.The values are of type
ResourceInfo. They are auto-created on access, so the properties can be assigned without preparations:kmock.resources['kopf.dev/v1/kopfexamples'].kind = 'KopfExample' kmock.resources['v1/pods'].shortnames = {'pod', 'po'}
The alternative is storing a new instance of
ResourceInfo:kmock.resources['v1/pods'] = ResourceInfo(shortnames={'pod', 'po'})
It is used in the API discovery only and is not used for payload validation. As such, all the information is fully optional. If the whole record or any fields are absent, they are returned either empty or guessed from the plural name of the resource (not grammatically correct, of course, but sufficient).
- Parameters:
resources (Mapping[str | Selectable | resource, ResourceInfo | ResourceDict] | None)
- load_data(data, /)[source]¶
Load the resource definitions from a data blob.
The data structure is opaque, i.e. you should not interpret it. Store it and use it exactly as generated by
kmock fetch resources. The data format may change in the future without a warning (with backwards compatibility for the old formats).
- load_bundled()[source]¶
Load the prepared bundled resource definitions from the package itself.
Beware: it might be outdated and incomplete. The file is maintained on the best effort basis. Usually, it is constructed from a recent K3s cluster in the default setup, with CRDs stripped, only with builtins.
For anything different from this quick and “good enough” option, build your own resource file with
kmock fetch resources --help.- Return type:
None
- exception kmock.KubernetesError(status=500, reason=None, message=None, details=None)[source]¶
Bases:
ExceptionAn error with its own K8s-specific payload & status code.
- Parameters:
- Return type:
None
- exception kmock.KubernetesNotFoundError(details=None, status=404, reason='Not Found', message='The URL or resource was not found')[source]¶
Bases:
KubernetesErrorAn error for the HTTP 404 errors for lacking URLs in Kubernetes.
- exception kmock.KubernetesEndpointNotFoundError(details=None, status=404, reason='Endpoint Not Found', message="The URL was not found and does not match any known pattern. Declare it as: kmock['/endpoint'] << b'some payload'")[source]¶
Bases:
KubernetesNotFoundErrorAn error for the HTTP 404 errors for unknown endpoint in Kubernetes.
- exception kmock.KubernetesResourceNotFoundError(details=None, status=404, reason='Resource Not Found', message="The resource is not declared in the Kubernetes server. Declare it either way, e.g.: kmock.resources['group/v1/plural'] = {}")[source]¶
Bases:
KubernetesNotFoundErrorAn error for the HTTP 404 errors for unknown resources in Kubernetes.
- exception kmock.KubernetesObjectNotFoundError(details=None, status=404, reason='Object Not Found', message="The object is not found in the Kubernetes server for a known resource. Add it as: kmock.objects[res, 'ns', 'name'] = {'spec': ...}")[source]¶
Bases:
KubernetesNotFoundErrorAn error for the HTTP 404 errors for unknown objects in Kubernetes.