crossplane-function-pythonic 0.1.4__py3-none-any.whl → 0.2.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.
@@ -0,0 +1,2 @@
1
+ # This is set at build time, using "hatch version"
2
+ __version__ = "0.2.0"
@@ -0,0 +1,2 @@
1
+ from . import main
2
+ main.main()
@@ -0,0 +1,102 @@
1
+
2
+ import logging
3
+ import pathlib
4
+ import sys
5
+
6
+
7
+ class Command:
8
+ name = None
9
+ command = None
10
+ description = None
11
+
12
+ @classmethod
13
+ def create(cls, subparsers):
14
+ parser = subparsers.add_parser(cls.name, help=cls.help, description=cls.description)
15
+ parser.set_defaults(command=cls)
16
+ cls.add_parser_arguments(parser)
17
+
18
+ @classmethod
19
+ def add_parser_arguments(cls, parser):
20
+ pass
21
+
22
+ @classmethod
23
+ def add_function_arguments(cls, parser):
24
+ parser.add_argument(
25
+ '--debug', '-d',
26
+ action='store_true',
27
+ help='Emit debug logs.',
28
+ )
29
+ parser.add_argument(
30
+ '--log-name-width',
31
+ type=int,
32
+ default=40,
33
+ metavar='WIDTH',
34
+ help='Width of the logger name in the log output, default 40.',
35
+ )
36
+ parser.add_argument(
37
+ '--python-path',
38
+ action='append',
39
+ default=[],
40
+ metavar='DIRECTORY',
41
+ help='Filing system directories to add to the python path.',
42
+ )
43
+ parser.add_argument(
44
+ '--render-unknowns', '-u',
45
+ action='store_true',
46
+ help='Render resources with unknowns, useful during local development.'
47
+ )
48
+ parser.add_argument(
49
+ '--allow-oversize-protos',
50
+ action='store_true',
51
+ help='Allow oversized protobuf messages',
52
+ )
53
+
54
+ def __init__(self, args):
55
+ self.args = args
56
+ self.initialize()
57
+
58
+ def initialize(self):
59
+ pass
60
+
61
+ def initialize_function(self):
62
+ formatter = Formatter(self.args.log_name_width)
63
+ handler = logging.StreamHandler(sys.stdout)
64
+ handler.setFormatter(formatter)
65
+ logger = logging.getLogger()
66
+ logger.handlers = [handler]
67
+ logger.setLevel(logging.DEBUG if self.args.debug else logging.INFO)
68
+
69
+ for path in reversed(self.args.python_path):
70
+ sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve()))
71
+
72
+ if self.args.allow_oversize_protos:
73
+ from google.protobuf.internal import api_implementation
74
+ if api_implementation._c_module:
75
+ api_implementation._c_module.SetAllowOversizeProtos(True)
76
+
77
+ async def run(self):
78
+ raise NotImplementedError()
79
+
80
+
81
+ class Formatter(logging.Formatter):
82
+ def __init__(self, name_width):
83
+ super(Formatter, self).__init__(
84
+ f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}",
85
+ '%Y-%m-%d %H:%M:%S',
86
+ '{',
87
+ )
88
+ self.name_width = name_width
89
+
90
+ def format(self, record):
91
+ record.sname = record.name
92
+ extra = len(record.sname) - self.name_width
93
+ if extra > 0:
94
+ names = record.sname.split('.')
95
+ for ix, name in enumerate(names):
96
+ if len(name) > extra:
97
+ names[ix] = name[extra:]
98
+ break
99
+ names[ix] = name[:1]
100
+ extra -= len(name) - 1
101
+ record.sname = '.'.join(names)
102
+ return super(Formatter, self).format(record)
@@ -47,7 +47,8 @@ class BaseComposite:
47
47
  self.spec = self.observed.spec
48
48
  self.status = self.desired.status
49
49
  self.conditions = Conditions(observed, self.response)
50
- self.events = Events(self.response)
50
+ self.results = Results(self.response)
51
+ self.events = Results(self.response) # Deprecated, use self.results
51
52
 
52
53
  @property
53
54
  def ttl(self):
@@ -449,6 +450,32 @@ class Conditions:
449
450
  def __getitem__(self, type):
450
451
  return Condition(self, type)
451
452
 
453
+ def __bool__(self):
454
+ if self._response is not None:
455
+ if self._response.conditions:
456
+ return True
457
+ if self._observed.resource.status.conditions:
458
+ return True
459
+ return False
460
+
461
+ def __len__(self):
462
+ return len(self._types())
463
+
464
+ def __iter__(self):
465
+ for type in self._types():
466
+ yield self[type]
467
+
468
+ def _types(self):
469
+ types = set()
470
+ if self._response is not None:
471
+ for condition in self._response.conditions:
472
+ if condition.type:
473
+ types.add(str(condition.type))
474
+ for condition in self._observed.resource.status.conditions:
475
+ if condition.type:
476
+ types.add(str(condition.type))
477
+ return sorted(types)
478
+
452
479
 
453
480
  class Condition(protobuf.ProtobufValue):
454
481
  def __init__(self, conditions, type):
@@ -567,42 +594,42 @@ class Condition(protobuf.ProtobufValue):
567
594
  return self._conditions._response.conditions.append(condition)
568
595
 
569
596
 
570
- class Events:
597
+ class Results:
571
598
  def __init__(self, response):
572
599
  self._results = response.results
573
600
 
574
601
  def info(self, reason=_notset, message=_notset, claim=_notset):
575
- event = Event(self._results.append())
576
- event.info = True
602
+ result = Result(self._results.append())
603
+ result.info = True
577
604
  if reason != _notset:
578
- event.reason = reason
605
+ result.reason = reason
579
606
  if message != _notset:
580
- event.message = message
607
+ result.message = message
581
608
  if claim != _notset:
582
- event.claim = claim
583
- return event
609
+ result.claim = claim
610
+ return result
584
611
 
585
612
  def warning(self, reason=_notset, message=_notset, claim=_notset):
586
- event = Event(self._results.append())
587
- event.warning = True
613
+ result = Result(self._results.append())
614
+ result.warning = True
588
615
  if reason != _notset:
589
- event.reason = reason
616
+ result.reason = reason
590
617
  if message != _notset:
591
- event.message = message
618
+ result.message = message
592
619
  if claim != _notset:
593
- event.claim = claim
594
- return event
620
+ result.claim = claim
621
+ return result
595
622
 
596
623
  def fatal(self, reason=_notset, message=_notset, claim=_notset):
597
- event = Event(self._results.append())
598
- event.fatal = True
624
+ result = Result(self._results.append())
625
+ result.fatal = True
599
626
  if reason != _notset:
600
- event.reason = reason
627
+ result.reason = reason
601
628
  if message != _notset:
602
- event.message = message
629
+ result.message = message
603
630
  if claim != _notset:
604
- event.claim = claim
605
- return event
631
+ result.claim = claim
632
+ return result
606
633
 
607
634
  def __bool__(self):
608
635
  return len(self) > 0
@@ -612,15 +639,15 @@ class Events:
612
639
 
613
640
  def __getitem__(self, key):
614
641
  if key >= len(self._results):
615
- return Event()
616
- return Event(self._results[key])
642
+ return Result()
643
+ return Result(self._results[key])
617
644
 
618
645
  def __iter__(self):
619
646
  for ix in range(len(self._results)):
620
647
  yield self[ix]
621
648
 
622
649
 
623
- class Event:
650
+ class Result:
624
651
  def __init__(self, result=None):
625
652
  self._result = result
626
653
 
@@ -24,10 +24,14 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
24
24
  self.clazzes = {}
25
25
 
26
26
  def invalidate_module(self, module):
27
- self.clazzes.clear()
28
- if module in sys.modules:
29
- del sys.modules[module]
27
+ ix = len(module)
28
+ while ix > 0:
29
+ module = module[:ix]
30
+ if module in sys.modules:
31
+ del sys.modules[module]
32
+ ix = module.rfind('.')
30
33
  importlib.invalidate_caches()
34
+ self.clazzes.clear()
31
35
 
32
36
  async def RunFunction(
33
37
  self, request: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext
@@ -44,7 +48,7 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
44
48
  name.append(composite['metadata']['name'])
45
49
  logger = logging.getLogger('.'.join(name))
46
50
 
47
- if composite['apiVersion'] == 'pythonic.fortra.com/v1alpha1' and composite['kind'] == 'Composite':
51
+ if composite['apiVersion'] in ('pythonic.crossplane.io/v1alpha1', 'pythonic.fortra.com/v1alpha1') and composite['kind'] == 'Composite':
48
52
  if 'spec' not in composite or 'composite' not in composite['spec']:
49
53
  return self.fatal(request, logger, 'Missing spec "composite"')
50
54
  single_use = True
@@ -272,30 +276,30 @@ class FunctionRunner(grpcv1.FunctionRunnerService):
272
276
  reason = 'FatalUnknowns'
273
277
  message = f"Observed resources with unknowns: {','.join(fatalResources)}"
274
278
  status = False
275
- event = composite.events.fatal
279
+ result = composite.results.fatal
276
280
  elif warningResources:
277
281
  level = composite.logger.warning
278
282
  reason = 'ObservedUnknowns'
279
283
  message = f"Observed resources with unknowns: {','.join(warningResources)}"
280
284
  status = False
281
- event = composite.events.warning
285
+ result = composite.results.warning
282
286
  elif unknownResources:
283
287
  level = composite.logger.info
284
288
  reason = 'DesiredUnknowns'
285
289
  message = f"Desired resources with unknowns: {','.join(unknownResources)}"
286
290
  status = False
287
- event = composite.events.info
291
+ result = composite.results.info
288
292
  else:
289
293
  level = None
290
294
  reason = 'AllComposed'
291
295
  message = 'All resources are composed'
292
296
  status = True
293
- event = None
297
+ result = None
294
298
  if not self.debug and level:
295
299
  level(message)
296
300
  composite.conditions.ResourcesComposed(reason, message, status)
297
- if event:
298
- event(reason, message)
301
+ if result:
302
+ result(reason, message)
299
303
 
300
304
  def process_auto_readies(self, composite):
301
305
  for name, resource in composite.resources:
@@ -0,0 +1,123 @@
1
+
2
+ import asyncio
3
+ import os
4
+ import pathlib
5
+ import shlex
6
+ import signal
7
+ import sys
8
+
9
+ import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
10
+ import grpc
11
+
12
+ from . import (
13
+ command,
14
+ function,
15
+ )
16
+
17
+
18
+ class Command(command.Command):
19
+ name = 'grpc'
20
+ help = 'Run function-pythonic gRPC server'
21
+
22
+ @classmethod
23
+ def add_parser_arguments(cls, parser):
24
+ cls.add_function_arguments(parser)
25
+ parser.add_argument(
26
+ '--address',
27
+ default='0.0.0.0:9443',
28
+ help='Address to listen on for gRPC connections, default: 0.0.0.0:9443',
29
+ )
30
+ parser.add_argument(
31
+ '--tls-certs-dir',
32
+ default=os.getenv('TLS_SERVER_CERTS_DIR'),
33
+ metavar='DIRECTORY',
34
+ help='Serve using TLS certificates.',
35
+ )
36
+ parser.add_argument(
37
+ '--insecure',
38
+ action='store_true',
39
+ help='Run without mTLS credentials, --tls-certs-dir will be ignored.',
40
+ )
41
+ parser.add_argument(
42
+ '--packages',
43
+ action='store_true',
44
+ help='Discover python packages from function-pythonic ConfigMaps.'
45
+ )
46
+ parser.add_argument(
47
+ '--packages-secrets',
48
+ action='store_true',
49
+ help='Also Discover python packages from function-pythonic Secrets.'
50
+ )
51
+ parser.add_argument(
52
+ '--packages-namespace',
53
+ action='append',
54
+ default=[],
55
+ metavar='NAMESPACE',
56
+ help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.',
57
+ )
58
+ parser.add_argument(
59
+ '--packages-dir',
60
+ default='./pythonic-packages',
61
+ metavar='DIRECTORY',
62
+ help='Directory to store discovered function-pythonic ConfigMaps to, defaults "<cwd>/pythonic-packages"'
63
+ )
64
+ parser.add_argument(
65
+ '--pip-install',
66
+ metavar='INSTALL',
67
+ help='Pip install command to install additional Python packages.'
68
+ )
69
+
70
+ def initialize(self):
71
+ if not self.args.tls_certs_dir and not self.args.insecure:
72
+ print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
73
+ sys.exit(1)
74
+
75
+ if self.args.pip_install:
76
+ import pip._internal.cli.main
77
+ pip._internal.cli.main.main(['install', '--user', *shlex.split(self.args.pip_install)])
78
+
79
+ self.initialize_function()
80
+
81
+ # enables read only volumes or mismatched uid volumes
82
+ sys.dont_write_bytecode = True
83
+
84
+ async def run(self):
85
+ grpc.aio.init_grpc_aio()
86
+ grpc_runner = function.FunctionRunner(self.args.debug, self.args.render_unknowns)
87
+ grpc_server = grpc.aio.server()
88
+ grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
89
+ if self.args.insecure:
90
+ grpc_server.add_insecure_port(self.args.address)
91
+ else:
92
+ certs = pathlib.Path(self.args.tls_certs_dir).expanduser().resolve()
93
+ grpc_server.add_secure_port(
94
+ self.args.address,
95
+ grpc.ssl_server_credentials(
96
+ private_key_certificate_chain_pairs=[(
97
+ (certs / 'tls.key').read_bytes(),
98
+ (certs / 'tls.crt').read_bytes(),
99
+ )],
100
+ root_certificates=(certs / 'ca.crt').read_bytes(),
101
+ require_client_auth=True,
102
+ ),
103
+ )
104
+ await grpc_server.start()
105
+
106
+ if self.args.packages:
107
+ from . import packages
108
+ async with asyncio.TaskGroup() as tasks:
109
+ tasks.create_task(grpc_server.wait_for_termination())
110
+ tasks.create_task(packages.operator(
111
+ grpc_server,
112
+ grpc_runner,
113
+ self.args.packages_secrets,
114
+ self.args.packages_namespace,
115
+ self.args.packages_dir,
116
+ ))
117
+ else:
118
+ def stop():
119
+ asyncio.ensure_future(grpc_server.stop(5))
120
+ loop = asyncio.get_event_loop()
121
+ loop.add_signal_handler(signal.SIGINT, stop)
122
+ loop.add_signal_handler(signal.SIGTERM, stop)
123
+ await grpc_server.wait_for_termination()
@@ -1,197 +1,27 @@
1
- """The composition function's main CLI."""
1
+ """The function-pythonic's main CLI."""
2
2
 
3
3
  import argparse
4
4
  import asyncio
5
- import logging
6
- import os
7
- import pathlib
8
- import shlex
9
- import signal
10
5
  import sys
11
- import traceback
12
6
 
13
- import crossplane.function.logging
14
- import crossplane.function.proto.v1.run_function_pb2_grpc as grpcv1
15
- import grpc
16
-
17
- from . import function
7
+ from . import (
8
+ grpc,
9
+ render,
10
+ version,
11
+ )
18
12
 
19
13
 
20
14
  def main():
21
- asyncio.run(Main().main())
22
-
23
-
24
- class Main:
25
- async def main(self):
26
- parser = argparse.ArgumentParser('Crossplane Function Pythonic')
27
- parser.add_argument(
28
- '--debug', '-d',
29
- action='store_true',
30
- help='Emit debug logs.',
31
- )
32
- parser.add_argument(
33
- '--log-name-width',
34
- type=int,
35
- default=40,
36
- metavar='WIDTH',
37
- help='Width of the logger name in the log output, default 40',
38
- )
39
- parser.add_argument(
40
- '--address',
41
- default='0.0.0.0:9443',
42
- help='Address to listen on for gRPC connections, default: 0.0.0.0:9443',
43
- )
44
- parser.add_argument(
45
- '--tls-certs-dir',
46
- default=os.getenv('TLS_SERVER_CERTS_DIR'),
47
- metavar='DIRECTORY',
48
- help='Serve using TLS certificates.',
49
- )
50
- parser.add_argument(
51
- '--insecure',
52
- action='store_true',
53
- help='Run without mTLS credentials, --tls-certs-dir will be ignored.',
54
- )
55
- parser.add_argument(
56
- '--packages',
57
- action='store_true',
58
- help='Discover python packages from function-pythonic ConfigMaps.'
59
- )
60
- parser.add_argument(
61
- '--packages-secrets',
62
- action='store_true',
63
- help='Also Discover python packages from function-pythonic Secrets.'
64
- )
65
- parser.add_argument(
66
- '--packages-namespace',
67
- action='append',
68
- default=[],
69
- metavar='NAMESPACE',
70
- help='Namespaces to discover function-pythonic ConfigMaps in, default is cluster wide.',
71
- )
72
- parser.add_argument(
73
- '--packages-dir',
74
- default='./pythonic-packages',
75
- metavar='DIRECTORY',
76
- help='Directory to store discovered function-pythonic ConfigMaps to, defaults "<cwd>/pythonic-packages"'
77
- )
78
- parser.add_argument(
79
- '--pip-install',
80
- metavar='COMMAND',
81
- help='Pip install command to install additional Python packages.'
82
- )
83
- parser.add_argument(
84
- '--python-path',
85
- action='append',
86
- default=[],
87
- metavar='DIRECTORY',
88
- help='Filing system directories to add to the python path',
89
- )
90
- parser.add_argument(
91
- '--allow-oversize-protos',
92
- action='store_true',
93
- help='Allow oversized protobuf messages'
94
- )
95
- parser.add_argument(
96
- '--render-unknowns',
97
- action='store_true',
98
- help='Render resources with unknowns, useful during local develomment'
99
- )
100
- args = parser.parse_args()
101
- if not args.tls_certs_dir and not args.insecure:
102
- print('Either --tls-certs-dir or --insecure must be specified', file=sys.stderr)
103
- sys.exit(1)
104
-
105
- if args.pip_install:
106
- import pip._internal.cli.main
107
- pip._internal.cli.main.main(['install', '--user', *shlex.split(args.pip_install)])
108
-
109
- for path in reversed(args.python_path):
110
- sys.path.insert(0, str(pathlib.Path(path).expanduser().resolve()))
111
-
112
- self.configure_logging(args)
113
- # enables read only volumes or mismatched uid volumes
114
- sys.dont_write_bytecode = True
115
- await self.run(args)
116
-
117
- # Allow for independent running of function-pythonic
118
- async def run(self, args):
119
- if args.allow_oversize_protos:
120
- from google.protobuf.internal import api_implementation
121
- if api_implementation._c_module:
122
- api_implementation._c_module.SetAllowOversizeProtos(True)
123
-
124
- grpc.aio.init_grpc_aio()
125
- grpc_runner = function.FunctionRunner(args.debug, args.render_unknowns)
126
- grpc_server = grpc.aio.server()
127
- grpcv1.add_FunctionRunnerServiceServicer_to_server(grpc_runner, grpc_server)
128
- if args.insecure:
129
- grpc_server.add_insecure_port(args.address)
130
- else:
131
- certs = pathlib.Path(args.tls_certs_dir).expanduser().resolve()
132
- grpc_server.add_secure_port(
133
- args.address,
134
- grpc.ssl_server_credentials(
135
- private_key_certificate_chain_pairs=[(
136
- (certs / 'tls.key').read_bytes(),
137
- (certs / 'tls.crt').read_bytes(),
138
- )],
139
- root_certificates=(certs / 'ca.crt').read_bytes(),
140
- require_client_auth=True,
141
- ),
142
- )
143
- await grpc_server.start()
144
-
145
- if args.packages:
146
- from . import packages
147
- async with asyncio.TaskGroup() as tasks:
148
- tasks.create_task(grpc_server.wait_for_termination())
149
- tasks.create_task(packages.operator(
150
- grpc_server,
151
- grpc_runner,
152
- args.packages_secrets,
153
- args.packages_namespace,
154
- args.packages_dir,
155
- ))
156
- else:
157
- def stop():
158
- asyncio.ensure_future(grpc_server.stop(5))
159
- loop = asyncio.get_event_loop()
160
- loop.add_signal_handler(signal.SIGINT, stop)
161
- loop.add_signal_handler(signal.SIGTERM, stop)
162
- await grpc_server.wait_for_termination()
163
-
164
- def configure_logging(self, args):
165
- formatter = Formatter(args.log_name_width)
166
- handler = logging.StreamHandler()
167
- handler.setFormatter(formatter)
168
- logger = logging.getLogger()
169
- logger.handlers = [handler]
170
- logger.setLevel(logging.DEBUG if args.debug else logging.INFO)
171
-
172
-
173
- class Formatter(logging.Formatter):
174
- def __init__(self, name_width):
175
- super(Formatter, self).__init__(
176
- f"[{{asctime}}.{{msecs:03.0f}}] {{sname:{name_width}.{name_width}}} [{{levelname:8.8}}] {{message}}",
177
- '%Y-%m-%d %H:%M:%S',
178
- '{',
179
- )
180
- self.name_width = name_width
181
-
182
- def format(self, record):
183
- record.sname = record.name
184
- extra = len(record.sname) - self.name_width
185
- if extra > 0:
186
- names = record.sname.split('.')
187
- for ix, name in enumerate(names):
188
- if len(name) > extra:
189
- names[ix] = name[extra:]
190
- break
191
- names[ix] = name[:1]
192
- extra -= len(name) - 1
193
- record.sname = '.'.join(names)
194
- return super(Formatter, self).format(record)
15
+ parser = argparse.ArgumentParser('Crossplane Function Pythonic')
16
+ subparsers = parser.add_subparsers(title='Command', metavar='')
17
+ grpc.Command.create(subparsers)
18
+ render.Command.create(subparsers)
19
+ version.Command.create(subparsers)
20
+ args = parser.parse_args()
21
+ if not hasattr(args, 'command'):
22
+ parser.print_help()
23
+ sys.exit(1)
24
+ asyncio.run(args.command(args).run())
195
25
 
196
26
 
197
27
  if __name__ == '__main__':
@@ -55,7 +55,6 @@ def B64Decode(string):
55
55
  string = str(string)
56
56
  return base64.b64decode(string.encode('utf-8')).decode('utf-8')
57
57
 
58
- B64Decode = lambda s: base64.b64decode(s.encode('utf-8')).decode('utf-8')
59
58
 
60
59
  class Message:
61
60
  def __init__(self, parent, key, descriptor, message=_Unknown, readOnly=False):
@@ -81,7 +80,7 @@ class Message:
81
80
  value = getattr(self._message, key)
82
81
  if value is _Unknown and field.has_default_value:
83
82
  value = field.default_value
84
- if field.label == field.LABEL_REPEATED:
83
+ if field.is_repeated:
85
84
  if field.type == field.TYPE_MESSAGE and field.message_type.GetOptions().map_entry:
86
85
  value = MapMessage(self, key, field.message_type.fields_by_name['value'], value, self._readOnly)
87
86
  else:
@@ -450,6 +449,10 @@ class RepeatedMessage:
450
449
  raise ValueError(f"{self._readOnly} is read only")
451
450
  if self._messages is _Unknown:
452
451
  self.__dict__['_messages'] = self._parent._create_child(self._key)
452
+ if key == append:
453
+ key = len(self._messages)
454
+ elif key < 0:
455
+ key = len(self._messages) + key
453
456
  while key >= len(self._messages):
454
457
  self._messages.add()
455
458
  return self._messages[key]
@@ -1050,6 +1053,10 @@ class Value:
1050
1053
  values = self._value.list_value.values
1051
1054
  else:
1052
1055
  values = self._value.values
1056
+ if key == append:
1057
+ key = len(values)
1058
+ elif key < 0:
1059
+ key = len(values) + key
1053
1060
  while key >= len(values):
1054
1061
  values.add()
1055
1062
  values[key].Clear()