crossplane-function-pythonic 0.6.2__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,115 +5,165 @@ import importlib
5
5
  import inspect
6
6
  import logging
7
7
  import sys
8
+ import typing
8
9
 
9
10
  import grpc
11
+ from google.protobuf.struct_pb2 import Struct
12
+
13
+ from crossplane.pythonic.composite import BaseComposite
14
+
15
+ from .. import pythonic
16
+ from . import protobuf
10
17
  from .proto.v1 import run_function_pb2 as fnv1
11
18
  from .proto.v1 import run_function_pb2_grpc as grpcv1
12
- from .. import pythonic
13
19
 
14
20
  logger = logging.getLogger(__name__)
15
21
 
16
22
 
17
- class FunctionRunner(grpcv1.FunctionRunnerService):
23
+ class FunctionRunner(grpcv1.FunctionRunnerServiceServicer):
18
24
  """A FunctionRunner handles gRPC RunFunctionRequests."""
19
25
 
20
- def __init__(self, renderUnknowns=False, crossplane_v1=False):
26
+ def __init__(self, renderUnknowns: bool = False, crossplane_v1: bool = False):
21
27
  """Create a new FunctionRunner."""
22
- self.renderUnknowns = renderUnknowns
23
- self.crossplane_v1 = crossplane_v1
24
- self.clazzes = {}
28
+ self.renderUnknowns: bool = renderUnknowns
29
+ self.crossplane_v1: bool = crossplane_v1
30
+ self.clazzes: dict[str, type[pythonic.BaseComposite]] = {}
25
31
 
26
- def invalidate_module(self, module):
32
+ def invalidate_module(self, module: str) -> None:
27
33
  ix = len(module)
28
34
  while ix > 0:
29
35
  module = module[:ix]
30
36
  if module in sys.modules:
31
37
  del sys.modules[module]
32
- ix = module.rfind('.')
38
+ ix = module.rfind(".")
33
39
  importlib.invalidate_caches()
34
40
  self.clazzes.clear()
35
41
 
36
42
  async def RunFunction(
37
- self, request: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
43
+ self,
44
+ request: fnv1.RunFunctionRequest,
45
+ context: grpc.aio.ServicerContext | None = None,
38
46
  ) -> fnv1.RunFunctionResponse:
39
47
  try:
40
48
  return await self.run_function(request)
41
- except Exception as e:
42
- return self.fatal(request, logger, 'RunFunction', e)
49
+ except Exception as e: # noqa: BLE001
50
+ return self.fatal(request, logger, "RunFunction", e)
43
51
 
44
- async def run_function(self, request):
45
- composite = request.observed.composite.resource
52
+ async def run_function(
53
+ self, request: fnv1.RunFunctionRequest
54
+ ) -> fnv1.RunFunctionResponse:
55
+ composite: Struct = request.observed.composite.resource
46
56
  if composite and composite.fields:
47
- name = list(reversed(composite['apiVersion'].split('/')[0].split('.')))
48
- name.append(composite['kind'])
49
- name.append(composite['metadata']['name'])
50
- logger = logging.getLogger('.'.join(name))
51
- if 'inlined' in request.input and request.input['inlined']:
52
- if 'spec' not in composite or request.input['inlined'] not in composite['spec']:
53
- return self.fatal(request, logger, f"Missing inlined spec.{request.input['inlined']}")
54
- composite = composite['spec'][request.input['inlined']]
57
+ name: list[str] = list(
58
+ reversed(str(composite["apiVersion"]).split("/")[0].split("."))
59
+ )
60
+ name.append(str(composite["kind"]))
61
+ name.append(str(typing.cast(Struct, composite["metadata"])["name"]))
62
+ logger = logging.getLogger(".".join(name))
63
+ if "inlined" in request.input and request.input["inlined"]: # noqa: RUF019
64
+ if "spec" not in composite or str(
65
+ request.input["inlined"]
66
+ ) not in typing.cast(Struct, composite["spec"]):
67
+ return self.fatal(
68
+ request,
69
+ logger,
70
+ f"Missing inlined spec.{request.input['inlined']}",
71
+ )
72
+ composite: str = str(
73
+ typing.cast(Struct, composite["spec"])[
74
+ str(request.input["inlined"])
75
+ ],
76
+ )
55
77
  else:
56
- if 'composite' not in request.input:
57
- return self.fatal(request, logger, 'Missing composition input "composite"')
58
- composite = request.input['composite']
78
+ if "composite" not in request.input:
79
+ return self.fatal(
80
+ request, logger, 'Missing composition input "composite"'
81
+ )
82
+ composite: str = str(request.input["composite"])
59
83
  operation = False
60
84
  else:
61
- logger = logging.getLogger('operation')
62
- if 'composite' not in request.input:
63
- return self.fatal(request, logger, 'Missing operation input "composite"')
64
- composite = request.input['composite']
85
+ logger = logging.getLogger("operation")
86
+ if "composite" not in request.input:
87
+ return self.fatal(
88
+ request, logger, 'Missing operation input "composite"'
89
+ )
90
+ composite: str = str(request.input["composite"])
65
91
  operation = True
66
92
 
67
93
  # Ideally this is something the Function API provides
68
- if 'step' in request.input:
69
- step = request.input['step']
94
+ if "step" in request.input:
95
+ step = request.input["step"]
70
96
  else:
71
97
  step = str(hash(composite))
72
98
 
73
99
  clazz = self.clazzes.get(composite)
74
100
  if not clazz:
75
- if '\n' in composite:
101
+ if "\n" in composite:
76
102
  module = Module()
77
103
  try:
78
- exec(composite, module.__dict__)
79
- except Exception as e:
80
- return self.fatal(request, logger, 'Exec', e)
104
+ exec(composite, module.__dict__) # noqa: S102
105
+ except Exception as e: # noqa: BLE001
106
+ return self.fatal(request, logger, "Exec", e)
81
107
  for field in dir(module):
82
108
  value = getattr(module, field)
83
- if inspect.isclass(value) and issubclass(value, pythonic.BaseComposite) and value != pythonic.BaseComposite:
109
+ if (
110
+ inspect.isclass(value)
111
+ and issubclass(value, pythonic.BaseComposite)
112
+ and value != pythonic.BaseComposite
113
+ ):
84
114
  if clazz:
85
- return self.fatal(request, logger, 'Composite script has multiple BaseComposite classes')
115
+ return self.fatal(
116
+ request,
117
+ logger,
118
+ "Composite script has multiple BaseComposite classes",
119
+ )
86
120
  clazz = value
87
121
  if not clazz:
88
- return self.fatal(request, logger, 'Composite script does not have a BaseComposite class')
122
+ return self.fatal(
123
+ request,
124
+ logger,
125
+ "Composite script does not have a BaseComposite class",
126
+ )
89
127
  else:
90
- composite = composite.rsplit('.', 1)
128
+ composite: list[str] = composite.rsplit(".", 1)
91
129
  if len(composite) == 1:
92
- return self.fatal(request, logger, f"Composite class name does not include module: {composite[0]}")
130
+ return self.fatal(
131
+ request,
132
+ logger,
133
+ f"Composite class name does not include module: {composite[0]}",
134
+ )
93
135
  try:
94
136
  module = importlib.import_module(composite[0])
95
- except Exception as e:
96
- return self.fatal(request, logger, 'Import module', e)
137
+ except Exception as e: # noqa: BLE001
138
+ return self.fatal(request, logger, "Import module", e)
97
139
  clazz = getattr(module, composite[1], None)
98
140
  if not clazz:
99
- return self.fatal(request, logger, f"{composite[0]} does not define: {composite[1]}")
100
- composite = '.'.join(composite)
141
+ return self.fatal(
142
+ request,
143
+ logger,
144
+ f"{composite[0]} does not define: {composite[1]}",
145
+ )
146
+ composite: str = ".".join(composite)
101
147
  if not inspect.isclass(clazz):
102
148
  return self.fatal(request, logger, f"{composite} is not a class")
103
149
  if not issubclass(clazz, pythonic.BaseComposite):
104
- return self.fatal(request, logger, f"{composite} is not a subclass of BaseComposite")
150
+ return self.fatal(
151
+ request,
152
+ logger,
153
+ f"{composite} is not a subclass of BaseComposite",
154
+ )
105
155
  self.clazzes[composite] = clazz
106
156
 
107
157
  if operation:
108
- if '\n' in composite:
158
+ if "\n" in composite:
109
159
  logger = logging.getLogger(f"operation.{clazz.__name__}")
110
160
  else:
111
161
  logger = logging.getLogger(f"operation.{composite}")
112
162
 
113
163
  try:
114
- composite = clazz(self.crossplane_v1, request, logger)
115
- except Exception as e:
116
- return self.fatal(request, logger, 'Instantiate', e)
164
+ composite: BaseComposite = clazz(self.crossplane_v1, request, logger)
165
+ except Exception as e: # noqa: BLE001
166
+ return self.fatal(request, logger, "Instantiate", e)
117
167
 
118
168
  step = composite.context._pythonic[step]
119
169
  iteration = int(step.iteration) + 1
@@ -121,18 +171,22 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
121
171
  composite.context.iteration = iteration
122
172
  if iteration == 1 and not logger.isEnabledFor(logging.DEBUG):
123
173
  if len(composite.context._pythonic) == 1:
124
- logger.info('Starting compose')
174
+ logger.info("Starting compose")
125
175
  else:
126
- logger.info(f"Starting compose, {ordinal(len(composite.context._pythonic))} step")
176
+ logger.info(
177
+ f"Starting compose, {ordinal(len(composite.context._pythonic))} step"
178
+ )
127
179
  else:
128
- logger.debug(f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass")
180
+ logger.debug(
181
+ f"Starting compose, {ordinal(len(composite.context._pythonic))} step, {ordinal(iteration)} pass"
182
+ )
129
183
 
130
184
  try:
131
185
  result = composite.compose()
132
186
  if asyncio.iscoroutine(result):
133
187
  await result
134
- except Exception as e:
135
- return self.fatal(request, logger, 'Compose', e)
188
+ except Exception as e: # noqa: BLE001
189
+ return self.fatal(request, logger, "Compose", e)
136
190
 
137
191
  schemas = self.get_schemas(step, composite)
138
192
  requireds = self.get_requireds(step, composite)
@@ -145,20 +199,26 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
145
199
  self.process_usages(composite)
146
200
  self.process_unknowns(composite)
147
201
  # Perform auto ready on all resources.
148
- for name, resource in composite.resources:
149
- resource.ready
150
- logger.info('Completed compose')
202
+ for _, resource in composite.resources:
203
+ resource.ready # noqa: B018
204
+ logger.info("Completed compose")
151
205
 
152
- return composite.response._message
206
+ return typing.cast(fnv1.RunFunctionResponse, composite.response._message)
153
207
 
154
- def fatal(self, request, logger, message, exception=None):
208
+ def fatal(
209
+ self,
210
+ request: fnv1.RunFunctionRequest,
211
+ logger: logging.Logger,
212
+ message: str,
213
+ exception: Exception | None = None,
214
+ ) -> fnv1.RunFunctionResponse:
155
215
  if exception:
156
- message += ' exception'
216
+ message += " exception"
157
217
  logger.exception(message)
158
218
  m = str(exception)
159
219
  if not m:
160
220
  m = exception.__class__.__name__
161
- message += ': ' + m
221
+ message += ": " + m
162
222
  else:
163
223
  logger.error(message)
164
224
  return fnv1.RunFunctionResponse(
@@ -170,11 +230,11 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
170
230
  severity=fnv1.SEVERITY_FATAL,
171
231
  message=message,
172
232
  )
173
- ]
233
+ ],
174
234
  )
175
235
 
176
- def get_schemas(self, step, composite):
177
- schemas = []
236
+ def get_schemas(self, step: protobuf.Value, composite: BaseComposite) -> list[str]:
237
+ schemas: list[str] = []
178
238
  for name, schema in composite.schemas:
179
239
  if len(schema.kind) and len(schema.apiVersion):
180
240
  s = pythonic.Map(kind=schema.kind, apiVersion=schema.apiVersion)
@@ -183,8 +243,10 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
183
243
  schemas.append(name)
184
244
  return schemas
185
245
 
186
- def get_requireds(self, step, composite):
187
- requireds = []
246
+ def get_requireds(
247
+ self, step: protobuf.Value, composite: BaseComposite
248
+ ) -> list[str]:
249
+ requireds: list[str] = []
188
250
  for name, required in composite.requireds:
189
251
  if len(required.kind) and len(required.apiVersion):
190
252
  r = pythonic.Map(kind=required.kind, apiVersion=required.apiVersion)
@@ -199,11 +261,11 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
199
261
  requireds.append(name)
200
262
  return requireds
201
263
 
202
- def process_usages(self, composite):
264
+ def process_usages(self, composite: BaseComposite) -> None:
203
265
  if self.crossplane_v1:
204
- apiVersion = 'apiextensions.crossplane.io/v1beta1'
266
+ apiVersion = "apiextensions.crossplane.io/v1beta1"
205
267
  else:
206
- apiVersion = 'protection.crossplane.io/v1beta1'
268
+ apiVersion = "protection.crossplane.io/v1beta1"
207
269
  for _, resource in sorted(entry for entry in composite.resources):
208
270
  dependencies = resource.desired._getDependencies
209
271
  if dependencies:
@@ -216,34 +278,41 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
216
278
  resources = {}
217
279
  requireds = {}
218
280
  for destination, source in sorted(dependencies.items()):
219
- name = source.split('.')
220
- if (len(name) > 5 and
221
- name[0] in ('request', 'response') and
222
- name[1] in ('observed', 'desired') and
223
- name[2] == 'resources' and
224
- name[4] == 'resource'
281
+ name = source.split(".")
282
+ if (
283
+ len(name) > 5
284
+ and name[0] in ("request", "response")
285
+ and name[1] in ("observed", "desired")
286
+ and name[2] == "resources"
287
+ and name[4] == "resource"
225
288
  ):
226
289
  if name[3] not in resources:
227
290
  resources[name[3]] = []
228
- resources[name[3]].append(f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}")
229
- elif (len(name) > 5 and
230
- name[0] == 'request' and
231
- name[1] in ('required_resources', 'extra_resources') and
232
- name[3].startswith('items[') and name[3][-1] == ']' and
233
- name[4] == 'resource'
291
+ resources[name[3]].append(
292
+ f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}"
293
+ )
294
+ elif (
295
+ len(name) > 5
296
+ and name[0] == "request"
297
+ and name[1] in ("required_resources", "extra_resources")
298
+ and name[3].startswith("items[")
299
+ and name[3][-1] == "]"
300
+ and name[4] == "resource"
234
301
  ):
235
302
  key = (name[2], int(name[3][6:-1]))
236
303
  if key not in requireds:
237
304
  requireds[key] = []
238
- requireds[key].append(f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}")
305
+ requireds[key].append(
306
+ f"{'.'.join(destination.split('.')[5:])} = {'.'.join(name[5:])}"
307
+ )
239
308
  for name, dependencies in resources.items():
240
309
  source = composite.resources[name]
241
- name = [resource.name, str(source.kind)]
310
+ name: list[str] = [str(resource.name), str(source.kind)]
242
311
  if source.metadata.namespace:
243
312
  name.append(str(source.metadata.namespace))
244
313
  name.append(str(source.observed.metadata.name))
245
- usage = composite.resources['_'.join(name)](apiVersion, 'Usage')
246
- usage.spec.reason = '\n'.join(dependencies)
314
+ usage = composite.resources["_".join(name)](apiVersion, "Usage")
315
+ usage.spec.reason = "\n".join(dependencies)
247
316
  usage.spec.replayDeletion = True
248
317
  usage.spec.by.apiVersion = resource.apiVersion
249
318
  usage.spec.by.kind = resource.kind
@@ -253,22 +322,34 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
253
322
  usage.spec.of.resourceRef.name = source.observed.metadata.name
254
323
  if not self.crossplane_v1:
255
324
  if composite.metadata.namespace:
256
- if source.metadata.namespace and source.metadata.namespace != composite.metadata.namespace:
257
- usage.spec.of.resourceRef.namespace = source.metadata.namespace
325
+ if (
326
+ source.metadata.namespace
327
+ and source.metadata.namespace
328
+ != composite.metadata.namespace
329
+ ):
330
+ usage.spec.of.resourceRef.namespace = (
331
+ source.metadata.namespace
332
+ )
258
333
  elif resource.metadata.namespace:
259
334
  usage.metadata.namespace = resource.metadata.namespace
260
- if source.metadata.namespace and source.metadata.namespace != resource.metadata.namespace:
261
- usage.spec.of.resourceRef.namespace = source.metadata.namespace
335
+ if (
336
+ source.metadata.namespace
337
+ and source.metadata.namespace
338
+ != resource.metadata.namespace
339
+ ):
340
+ usage.spec.of.resourceRef.namespace = (
341
+ source.metadata.namespace
342
+ )
262
343
  else:
263
- usage.kind = 'ClusterUsage'
344
+ usage.kind = "ClusterUsage"
264
345
  for key, dependencies in requireds.items():
265
346
  source = composite.requireds[key[0]][key[1]]
266
- name = [resource.name, str(source.kind)]
347
+ name: list[str] = [str(resource.name), str(source.kind)]
267
348
  if source.metadata.namespace:
268
349
  name.append(str(source.metadata.namespace))
269
350
  name.append(str(source.metadata.name))
270
- usage = composite.resources['_'.join(name)](apiVersion, 'Usage')
271
- usage.spec.reason = '\n'.join(dependencies)
351
+ usage = composite.resources["_".join(name)](apiVersion, "Usage")
352
+ usage.spec.reason = "\n".join(dependencies)
272
353
  usage.spec.replayDeletion = True
273
354
  usage.spec.by.apiVersion = resource.apiVersion
274
355
  usage.spec.by.kind = resource.kind
@@ -278,19 +359,31 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
278
359
  usage.spec.of.resourceRef.name = source.observed.metadata.name
279
360
  if not self.crossplane_v1:
280
361
  if composite.metadata.namespace:
281
- if source.metadata.namespace and source.metadata.namespace != composite.metadata.namespace:
282
- usage.spec.of.resourceRef.namespace = source.metadata.namespace
362
+ if (
363
+ source.metadata.namespace
364
+ and source.metadata.namespace
365
+ != composite.metadata.namespace
366
+ ):
367
+ usage.spec.of.resourceRef.namespace = (
368
+ source.metadata.namespace
369
+ )
283
370
  elif resource.metadata.namespace:
284
371
  usage.metadata.namespace = resource.metadata.namespace
285
- if source.metadata.namespace and source.metadata.namespace != resource.metadata.namespace:
286
- usage.spec.of.resourceRef.namespace = source.metadata.namespace
372
+ if (
373
+ source.metadata.namespace
374
+ and source.metadata.namespace
375
+ != resource.metadata.namespace
376
+ ):
377
+ usage.spec.of.resourceRef.namespace = (
378
+ source.metadata.namespace
379
+ )
287
380
  else:
288
- usage.kind = 'ClusterUsage'
381
+ usage.kind = "ClusterUsage"
289
382
 
290
- def process_unknowns(self, composite):
291
- unknownResources = []
292
- warningResources = []
293
- fatalResources = []
383
+ def process_unknowns(self, composite: BaseComposite) -> None:
384
+ unknownResources: list[str] = []
385
+ warningResources: list[str] = []
386
+ fatalResources: list[str] = []
294
387
  for name, resource in sorted(entry for entry in composite.resources):
295
388
  unknowns = resource.desired._getUnknowns
296
389
  if unknowns:
@@ -300,7 +393,9 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
300
393
  if resource.observed:
301
394
  warningResources.append(name)
302
395
  warning = True
303
- if resource.unknownsFatal or (resource.unknownsFatal is None and composite.unknownsFatal):
396
+ if resource.unknownsFatal or (
397
+ resource.unknownsFatal is None and composite.unknownsFatal
398
+ ):
304
399
  fatalResources.append(name)
305
400
  fatal = True
306
401
  if composite.logger.isEnabledFor(logging.DEBUG):
@@ -308,11 +403,17 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
308
403
  destination = self.trimFullName(destination)
309
404
  source = self.trimFullName(source)
310
405
  if fatal:
311
- composite.logger.error(f'Observed unknown: {destination} = {source}')
406
+ composite.logger.error(
407
+ f"Observed unknown: {destination} = {source}"
408
+ )
312
409
  elif warning:
313
- composite.logger.warning(f'Observed unknown: {destination} = {source}')
410
+ composite.logger.warning(
411
+ f"Observed unknown: {destination} = {source}"
412
+ )
314
413
  else:
315
- composite.logger.debug(f'Desired unknown: {destination} = {source}')
414
+ composite.logger.debug(
415
+ f"Desired unknown: {destination} = {source}"
416
+ )
316
417
  if resource.observed:
317
418
  resource.desired._patchUnknowns(resource.observed)
318
419
  elif self.renderUnknowns:
@@ -322,26 +423,26 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
322
423
 
323
424
  if fatalResources:
324
425
  level = composite.logger.error
325
- reason = 'FatalUnknowns'
426
+ reason = "FatalUnknowns"
326
427
  message = f"Observed resources with unknowns: {','.join(fatalResources)}"
327
428
  status = False
328
429
  result = composite.results.fatal
329
430
  elif warningResources:
330
431
  level = composite.logger.warning
331
- reason = 'ObservedUnknowns'
432
+ reason = "ObservedUnknowns"
332
433
  message = f"Observed resources with unknowns: {','.join(warningResources)}"
333
434
  status = False
334
435
  result = composite.results.warning
335
436
  elif unknownResources:
336
437
  level = composite.logger.info
337
- reason = 'DesiredUnknowns'
438
+ reason = "DesiredUnknowns"
338
439
  message = f"Desired resources with unknowns: {','.join(unknownResources)}"
339
440
  status = False
340
441
  result = composite.results.info
341
442
  else:
342
443
  level = None
343
- reason = 'AllComposed'
344
- message = 'All resources are composed'
444
+ reason = "AllComposed"
445
+ message = "All resources are composed"
345
446
  status = True
346
447
  result = None
347
448
  if not composite.logger.isEnabledFor(logging.DEBUG) and level:
@@ -350,24 +451,27 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
350
451
  if result:
351
452
  result(reason, message)
352
453
 
353
- def trimFullName(self, name):
354
- name = name.split('.')
454
+ def trimFullName(self, name: str) -> str:
455
+ name: list[str] = name.split(".")
355
456
  for values in (
356
- ('request', 'observed', 'composite', 'resource'),
357
- ('response', 'desired', 'composite', 'resource'),
358
- ('request', 'observed', 'resources', None, 'resource'),
359
- ('response', 'desired', 'resources', None, 'resource'),
360
- ('request', 'required_resources', None, 'items', None, 'resource'),
361
- ('request', 'extra_resources', None, 'items', None, 'resource'),
457
+ ("request", "observed", "composite", "resource"),
458
+ ("response", "desired", "composite", "resource"),
459
+ ("request", "observed", "resources", None, "resource"),
460
+ ("response", "desired", "resources", None, "resource"),
461
+ ("request", "required_resources", None, "items", None, "resource"),
462
+ ("request", "extra_resources", None, "items", None, "resource"),
362
463
  ):
363
464
  if len(values) < len(name):
364
465
  ix = 0
365
466
  for iv, value in enumerate(values):
366
- if value:
367
- if value != name[ix]:
368
- if not name[ix].startswith(f"{values[iv]}[") or iv+1 >= len(values) or values[iv+1]:
369
- break
370
- continue
467
+ if value and value != name[ix]:
468
+ if (
469
+ not name[ix].startswith(f"{value}[")
470
+ or iv + 1 >= len(values)
471
+ or values[iv + 1]
472
+ ):
473
+ break
474
+ continue
371
475
  ix += 1
372
476
  else:
373
477
  ix = 0
@@ -376,22 +480,21 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
376
480
  if value == name[ix]:
377
481
  del name[ix]
378
482
  elif ix:
379
- name[ix-1] += name[ix][len(value):]
483
+ name[ix - 1] += name[ix][len(value) :]
380
484
  del name[ix]
381
485
  else:
382
- name[ix] = name[ix][len(value):]
486
+ name[ix] = name[ix][len(value) :]
383
487
  else:
384
488
  ix += 1
385
489
  break
386
- return '.'.join(name)
490
+ return ".".join(name)
387
491
 
388
492
 
389
- def ordinal(ix):
390
- ix = int(ix)
493
+ def ordinal(ix: int) -> str:
391
494
  if 11 <= (ix % 100) <= 13:
392
- suffix = 'th'
495
+ suffix = "th"
393
496
  else:
394
- suffix = ['th', 'st', 'nd', 'rd', 'th'][min(ix % 10, 4)]
497
+ suffix = ["th", "st", "nd", "rd", "th"][min(ix % 10, 4)]
395
498
  return str(ix) + suffix
396
499
 
397
500