fastbpmn 0.1.0__tar.gz

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.
Files changed (87) hide show
  1. fastbpmn-0.1.0/LICENSE +21 -0
  2. fastbpmn-0.1.0/PKG-INFO +491 -0
  3. fastbpmn-0.1.0/README.md +456 -0
  4. fastbpmn-0.1.0/pyproject.toml +128 -0
  5. fastbpmn-0.1.0/src/fastbpmn/__init__.py +15 -0
  6. fastbpmn-0.1.0/src/fastbpmn/aetpi/__init__.py +0 -0
  7. fastbpmn-0.1.0/src/fastbpmn/aetpi/logonly.py +262 -0
  8. fastbpmn-0.1.0/src/fastbpmn/aetpi/middleware/__init__.py +5 -0
  9. fastbpmn-0.1.0/src/fastbpmn/aetpi/middleware/variables.py +90 -0
  10. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/__init__.py +0 -0
  11. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/__init__.py +45 -0
  12. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/decoder.py +214 -0
  13. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/encoder.py +129 -0
  14. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/middleware.py +50 -0
  15. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/producer.py +211 -0
  16. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/queue.py +34 -0
  17. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/server.py +309 -0
  18. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/settings.py +52 -0
  19. fastbpmn-0.1.0/src/fastbpmn/aetpi/servers/camunda7/worker.py +448 -0
  20. fastbpmn-0.1.0/src/fastbpmn/aetpi/utils.py +214 -0
  21. fastbpmn-0.1.0/src/fastbpmn/camunda/__init__.py +37 -0
  22. fastbpmn-0.1.0/src/fastbpmn/camunda/asynchttp.py +340 -0
  23. fastbpmn-0.1.0/src/fastbpmn/camunda/config.py +11 -0
  24. fastbpmn-0.1.0/src/fastbpmn/camunda/datatypes.py +44 -0
  25. fastbpmn-0.1.0/src/fastbpmn/camunda/errors.py +110 -0
  26. fastbpmn-0.1.0/src/fastbpmn/camunda/json.py +37 -0
  27. fastbpmn-0.1.0/src/fastbpmn/camunda/models.py +55 -0
  28. fastbpmn-0.1.0/src/fastbpmn/camunda/processengine.py +735 -0
  29. fastbpmn-0.1.0/src/fastbpmn/camunda/utils.py +43 -0
  30. fastbpmn-0.1.0/src/fastbpmn/context/__init__.py +4 -0
  31. fastbpmn-0.1.0/src/fastbpmn/context/context.py +99 -0
  32. fastbpmn-0.1.0/src/fastbpmn/context/io.py +33 -0
  33. fastbpmn-0.1.0/src/fastbpmn/context/utils.py +32 -0
  34. fastbpmn-0.1.0/src/fastbpmn/errors/__init__.py +27 -0
  35. fastbpmn-0.1.0/src/fastbpmn/errors/base.py +42 -0
  36. fastbpmn-0.1.0/src/fastbpmn/errors/bpmn_error.py +35 -0
  37. fastbpmn-0.1.0/src/fastbpmn/errors/context.py +36 -0
  38. fastbpmn-0.1.0/src/fastbpmn/errors/process_engine.py +64 -0
  39. fastbpmn-0.1.0/src/fastbpmn/errors/task_failure_error.py +47 -0
  40. fastbpmn-0.1.0/src/fastbpmn/errors/types.py +5 -0
  41. fastbpmn-0.1.0/src/fastbpmn/fastbpmn.py +347 -0
  42. fastbpmn-0.1.0/src/fastbpmn/fastbpmn_types.py +35 -0
  43. fastbpmn-0.1.0/src/fastbpmn/lifespan/__init__.py +209 -0
  44. fastbpmn-0.1.0/src/fastbpmn/middleware/__init__.py +33 -0
  45. fastbpmn-0.1.0/src/fastbpmn/middleware/context/__init__.py +31 -0
  46. fastbpmn-0.1.0/src/fastbpmn/middleware/exceptions/__init__.py +178 -0
  47. fastbpmn-0.1.0/src/fastbpmn/middleware/exceptions/handler.py +167 -0
  48. fastbpmn-0.1.0/src/fastbpmn/middleware/exceptions/types.py +10 -0
  49. fastbpmn-0.1.0/src/fastbpmn/middleware/exceptions/utils.py +73 -0
  50. fastbpmn-0.1.0/src/fastbpmn/middleware/guard/__init__.py +33 -0
  51. fastbpmn-0.1.0/src/fastbpmn/middleware/guard/types.py +5 -0
  52. fastbpmn-0.1.0/src/fastbpmn/middleware/guard/utils.py +69 -0
  53. fastbpmn-0.1.0/src/fastbpmn/models/__init__.py +43 -0
  54. fastbpmn-0.1.0/src/fastbpmn/models/base.py +80 -0
  55. fastbpmn-0.1.0/src/fastbpmn/models/decoder.py +26 -0
  56. fastbpmn-0.1.0/src/fastbpmn/models/encoder.py +208 -0
  57. fastbpmn-0.1.0/src/fastbpmn/models/task.py +62 -0
  58. fastbpmn-0.1.0/src/fastbpmn/models/types/__init__.py +11 -0
  59. fastbpmn-0.1.0/src/fastbpmn/models/types/camunda7.py +237 -0
  60. fastbpmn-0.1.0/src/fastbpmn/process.py +124 -0
  61. fastbpmn-0.1.0/src/fastbpmn/processor/__init__.py +306 -0
  62. fastbpmn-0.1.0/src/fastbpmn/processor/routing.py +171 -0
  63. fastbpmn-0.1.0/src/fastbpmn/processor/task.py +78 -0
  64. fastbpmn-0.1.0/src/fastbpmn/readiness.py +13 -0
  65. fastbpmn-0.1.0/src/fastbpmn/result.py +162 -0
  66. fastbpmn-0.1.0/src/fastbpmn/task.py +114 -0
  67. fastbpmn-0.1.0/src/fastbpmn/task_group.py +93 -0
  68. fastbpmn-0.1.0/src/fastbpmn/types.py +11 -0
  69. fastbpmn-0.1.0/src/fastbpmn/utils/__init__.py +0 -0
  70. fastbpmn-0.1.0/src/fastbpmn/utils/asyncer.py +101 -0
  71. fastbpmn-0.1.0/src/fastbpmn/utils/asyncio.py +30 -0
  72. fastbpmn-0.1.0/src/fastbpmn/utils/concurrency.py +38 -0
  73. fastbpmn-0.1.0/src/fastbpmn/utils/config.py +57 -0
  74. fastbpmn-0.1.0/src/fastbpmn/utils/func.py +22 -0
  75. fastbpmn-0.1.0/src/fastbpmn/utils/inspect.py +64 -0
  76. fastbpmn-0.1.0/src/fastbpmn/utils/names.py +21 -0
  77. fastbpmn-0.1.0/src/squirrel/__init__.py +9 -0
  78. fastbpmn-0.1.0/src/squirrel/__main__.py +3 -0
  79. fastbpmn-0.1.0/src/squirrel/app.py +7 -0
  80. fastbpmn-0.1.0/src/squirrel/arguments.py +25 -0
  81. fastbpmn-0.1.0/src/squirrel/flavours/__init__.py +5 -0
  82. fastbpmn-0.1.0/src/squirrel/flavours/camunda7/__init__.py +0 -0
  83. fastbpmn-0.1.0/src/squirrel/flavours/camunda7/entrypoint.py +92 -0
  84. fastbpmn-0.1.0/src/squirrel/flavours/camunda7/settings.py +15 -0
  85. fastbpmn-0.1.0/src/squirrel/parser.py +54 -0
  86. fastbpmn-0.1.0/src/squirrel/run.py +84 -0
  87. fastbpmn-0.1.0/src/squirrel/settings.py +23 -0
fastbpmn-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yio gmbh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,491 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastbpmn
3
+ Version: 0.1.0
4
+ Summary: A framework allowing to write external tasks for various bpmn process engines.
5
+ Keywords: minions,yio,process engine,aetpi - async external task processing interfance
6
+ Author: Daniel Hofstetter, Fritz Diener
7
+ Author-email: Daniel Hofstetter <hello@yio.at>, Fritz Diener <hello@yio.at>
8
+ License-File: LICENSE
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Dist: aetpiref>=0.1.6,<1.0.0
12
+ Requires-Dist: aiolimiter>=1.2.1
13
+ Requires-Dist: asyncer~=0.0.2
14
+ Requires-Dist: culsans>=0.11.0
15
+ Requires-Dist: email-validator<=2.2
16
+ Requires-Dist: httpx>=0.24.1
17
+ Requires-Dist: httpx-auth~=0.18
18
+ Requires-Dist: pydantic>=2.0,<3.0
19
+ Requires-Dist: pydantic-settings>=2.13.1
20
+ Requires-Dist: pymdown-extensions~=10.5
21
+ Requires-Dist: python-dotenv~=1.0
22
+ Requires-Dist: python-magic~=0.4
23
+ Requires-Dist: rich>=14.3.3
24
+ Requires-Dist: structlog>=25.5.0
25
+ Requires-Dist: structlog-config>=0.10.0
26
+ Requires-Dist: tenacity>=9.1.4
27
+ Requires-Dist: typer>=0.24.1,<0.25.0
28
+ Requires-Python: >=3.11
29
+ Project-URL: Homepage, https://yio.at
30
+ Project-URL: Documentation, https://docs.yio.at/products/yio-minions
31
+ Project-URL: Repository, https://github.com/yiogmbh/fastbpmn.git
32
+ Project-URL: Issues, https://github.com/yiogmbh/fastbpmn/issues
33
+ Project-URL: Changelog, https://github.com/yiogmbh/fastbpmn/blob/main/CHANGELOG.md
34
+ Description-Content-Type: text/markdown
35
+
36
+ # fastbpmn
37
+ A framework allowing to write external tasks for various bpmn process engines.
38
+
39
+ ## Installation / Usage / Prerequisites
40
+
41
+ Ensure that the libmagic C library is installed on your system. See [python-magic](https://pypi.org/project/python-magic/) for installation on various systems.
42
+
43
+ On OSX use:
44
+ ```
45
+ brew install libmagic
46
+ ```
47
+
48
+ ## Usage Example
49
+
50
+ ```python
51
+ from fastbpmn import FastBPMN
52
+ from fastbpmn.camunda import ProcessEngine
53
+ from fastbpmn.models import BaseInputModel, BaseOutputModel
54
+
55
+ app = FastBPMN(name="Bob")
56
+
57
+
58
+ class OracleInput(BaseInputModel):
59
+ string_variable: str # Requires a process variable called string_variable
60
+ integer_variable: int # Requires a process variable called integer_variable
61
+
62
+
63
+ class OracleOutput(BaseOutputModel):
64
+ win_or_loose: bool # Sets the process variable win_or_loose in the end
65
+
66
+
67
+ @app.external_task(
68
+ topic="ask-oracle-delphi",
69
+ input_class=OracleInput,
70
+ output_class=OracleOutput,
71
+ )
72
+ async def ask_oracle_delphi(input_data: OracleInput):
73
+ """
74
+ External Task / DEMO
75
+ Delphi was a sacred precinct that served as the seat of Pythia, the major oracle
76
+ who was consulted about important decisions throughout the ancient classical world.
77
+ """
78
+ dummy_number = input_data.integer_variable + len(input_data.string_variable)
79
+
80
+ win_or_loose = bool(dummy_number % 2)
81
+
82
+ return OracleOutput(win_or_loose=win_or_loose)
83
+
84
+
85
+ @app.external_task(
86
+ topic="ask-oracle-dodona",
87
+ input_class=OracleInput,
88
+ output_class=OracleOutput,
89
+ )
90
+ async def ask_oracle_dodona(input_data: OracleInput):
91
+ """
92
+ External Task / DEMO
93
+ Dodona in Epirus in northwestern Greece was the oldest Hellenic oracle.
94
+ The earliest accounts in Homer describe Dodona as an oracle of Zeus.
95
+ """
96
+ dummy_number = input_data.integer_variable + len(input_data.string_variable)
97
+
98
+ win_or_loose = not bool(dummy_number % 2)
99
+
100
+ return OracleOutput(win_or_loose=win_or_loose)
101
+
102
+
103
+ # start the fastbpmn application using the included squirrel
104
+ if __name__ == '__main__':
105
+ import squirrel
106
+ from structlog_config import configure_logger
107
+ log = configure_logger()
108
+
109
+ #structlog.stdlib.recreate_defaults(log_level=logging.INFO)
110
+
111
+ squirrel.run(
112
+ app,
113
+ flavour="camunda7",
114
+
115
+ name="bob",
116
+ workers=10,
117
+ camunda_url="<your pe url>",
118
+ camunda_username="<your username>",
119
+ camunda_password="<your password>",
120
+ )
121
+ ```
122
+
123
+ #### Lifespan Handler
124
+
125
+ ```python
126
+ from fastbpmn import FastBPMN
127
+ from fastbpmn.camunda import ProcessEngine
128
+ from contextlib import asynccontextmanager
129
+
130
+ @asynccontextmanager
131
+ async def lifespan(app):
132
+ print("Done on startup ...")
133
+ yield
134
+ print("done on shutdown ...")
135
+
136
+ app = FastBPMN(
137
+ name="Bob",
138
+ lifespan=lifespan
139
+ )
140
+ ```
141
+
142
+ #### Retries
143
+
144
+ In order to handle errors with retries there is a special exception that should be
145
+ raised within your external tasks. The latter example shows the usage:
146
+
147
+ ```python
148
+ from fastbpmn import FastBPMN
149
+ from fastbpmn.errors import RetryExternalTask
150
+ from fastbpmn.camunda import ProcessEngine
151
+
152
+ app = FastBPMN(
153
+ name="Bob"
154
+ )
155
+
156
+
157
+ @app.external_task(
158
+ topic="last-forever",
159
+ input_class=None,
160
+ output_class=None,
161
+ )
162
+ async def retry_infinite(input_data: None) -> None:
163
+ print("I will last forever")
164
+ raise RetryExternalTask(retries=1) # No matter what happens we always tell the
165
+ # process engine to try once again ;-)
166
+
167
+
168
+ @app.external_task(
169
+ topic="try-5times",
170
+ input_class=None,
171
+ output_class=None,
172
+ retries=5
173
+ )
174
+ async def retry_infinite(input_data: None) -> None:
175
+ print("You should see me 5 or 6 times ...")
176
+ raise RetryExternalTask() # not specifying a number of retries within the
177
+ # exception leads to decrease of initial number
178
+ ```
179
+
180
+ #### No Input Values
181
+
182
+ It's possible to omit all the arguments if your external-task won't depend on input data.
183
+
184
+ ```python
185
+ from fastbpmn import FastBPMN
186
+ from fastbpmn.camunda import ProcessEngine
187
+
188
+ app = FastBPMN(
189
+ name="Bob"
190
+ )
191
+
192
+
193
+ @app.external_task(
194
+ topic="without-args"
195
+ )
196
+ async def no_args() -> None:
197
+ print("I'm almighty, I won't need any arguments. I will work anyway")
198
+ return
199
+ ```
200
+
201
+ #### Task/TaskProperties
202
+
203
+ Sometimes you might be interested in properties of the current Task or the TaskProperties in general.
204
+ You can declare an external task such that you will receive this objects for usage:
205
+
206
+ > **Attention** This is highly experimental and due to upcoming refactorings of the process engine interface the
207
+ > Task and TaskProperties Class are likely to change in the future.
208
+
209
+ ```python
210
+ from fastbpmn import FastBPMN
211
+ from fastbpmn.task import Task, TaskProperties
212
+ from fastbpmn.camunda import ProcessEngine
213
+
214
+ app = FastBPMN(
215
+ name="Bob"
216
+ )
217
+
218
+
219
+ @app.external_task(
220
+ topic="task-info",
221
+ input_class=None,
222
+ output_class=None,
223
+ )
224
+ async def print_taskinfo(input_data: None, task: Task, task_properties: TaskProperties) -> None:
225
+ print(f"TaskId: {task.id} - initial retries: {task_properties.retries}")
226
+ return
227
+ ```
228
+
229
+ #### Process
230
+
231
+ A Process is a useful Method to create more structured code.
232
+
233
+ ```python
234
+ from fastbpmn import FastBPMN, Process
235
+ from fastbpmn.task import Task, TaskProperties
236
+ from fastbpmn.camunda import ProcessEngine
237
+
238
+ app = FastBPMN(
239
+ name="Bob"
240
+ )
241
+
242
+ process_a = Process(
243
+ process_definition_key="ProzessA"
244
+ )
245
+
246
+
247
+ @process_a.external_task("print_a")
248
+ async def print_a(input_data: None) -> None:
249
+ print("Hello from a Prozess A Only Task.")
250
+ return
251
+
252
+
253
+ # put me in a different file if you want ;-)
254
+ process_b = Process(
255
+ process_definition_key="ProzessB"
256
+ )
257
+
258
+
259
+ @process_b.external_task("print_b")
260
+ async def print_b(input_data: None) -> None:
261
+ print("Hello from a Prozess B Only Task.")
262
+ return
263
+
264
+
265
+ # You can also attach a TaskHandler method to multiple Processes
266
+
267
+ async def print_common(input_data: None, task: Task, task_properties: TaskProperties) -> None:
268
+ print(f"Hello from a common task, i was executed by process ... {task.process_definition_key}.")
269
+ return
270
+
271
+
272
+ # Add the print common to processes wherever you like
273
+ # >-> The Topic can be different as well..
274
+ process_b.add_task("print_common", handler=print_common)
275
+ process_a.add_task("print_common", handler=print_common)
276
+
277
+
278
+ ```
279
+
280
+ #### Context
281
+
282
+ You can make use of a context within your external task. The context provides some useful features
283
+ (e.g. create of temporary files / directories).
284
+
285
+ ```python
286
+ from fastbpmn import FastBPMN, Process
287
+ from fastbpmn.context import Context, Delete
288
+ from fastbpmn.camunda import ProcessEngine
289
+
290
+ app = FastBPMN(
291
+ name="Bob"
292
+ )
293
+
294
+ process_a = Process(
295
+ process_definition_key="ProzessA"
296
+ )
297
+
298
+
299
+ @process_a.external_task("everlasting-file")
300
+ async def print_a(ctx: Context) -> None:
301
+ print("Hello from a Prozess A, I create a file that is not deleted ....")
302
+ file_path = ctx.temp_file(flags=Delete.NEVER)
303
+ return
304
+
305
+
306
+ @process_a.external_task("deleted-file")
307
+ async def print_b(ctx: Context) -> None:
308
+ print("Hello from a Prozess A, I create a file that deleted when I'm done ....")
309
+ file_path = ctx.temp_file(flags=Delete.ALWAYS)
310
+ return
311
+ ```
312
+
313
+ #### File Handling
314
+
315
+ There are several ways to deal with File variables in Camunda. The following example shows three use cases.
316
+
317
+ The assumption is the following:
318
+
319
+ - the process has two file variables called `pdf_file_var` and `png_file_var` in camunda
320
+ - there is an external task that needs to work with these files
321
+ - the `png_file_var` is only optional and might not be present
322
+ - there are three scenarios:
323
+ - the variable name in camunda are known
324
+ - the variable names in camunda are subject to change, but there are two other variables
325
+ holding the names (`pdf_file_var_name` and `png_file_var_name`)
326
+ - the variable names in camunda are subject to change as is the number of files, but there is
327
+ a list of variable names (`file_var_names`) that hold the names of the file variables
328
+
329
+ ```python
330
+ import asyncio
331
+ from functools import cached_property
332
+
333
+ from pydantic import computed_field, model_validator
334
+
335
+ from fastbpmn import FastBPMN, Process
336
+ from fastbpmn.context import Context
337
+ from fastbpmn.camunda import ProcessEngine
338
+ from fastbpmn.models import BaseInputModel, FileInfo, get_file_info_indirect
339
+
340
+ process_a = Process(
341
+ process_definition_key="ProzessA"
342
+ )
343
+ app = FastBPMN(name="Bob")
344
+
345
+
346
+ class Option1InputModel(BaseInputModel):
347
+ """
348
+ This is the first option to deal with file variables.
349
+
350
+ The variable names are known and can be used directly, that means there is a process
351
+ variable called `pdf_file_var` and `png_file_var` in camunda with type file.
352
+ """
353
+ pdf_file_var: FileInfo
354
+ png_file_var: FileInfo | None = None
355
+
356
+
357
+ class Option2InputModel(BaseInputModel):
358
+ """
359
+ This is the second option to deal with file variables.
360
+
361
+ The variable names are not known and are subject to change. But there are two other
362
+ variables (type string) that hold the names of the file variables.
363
+
364
+ Be aware of the computed properties and validators that are used to ensure that the
365
+ file variables are present as expected.
366
+ """
367
+ # if `pdf_file_var_name` is set to 'other_pdf_var' then a process variable called
368
+ # `other_pdf_var` is expected to be present in camunda with type file. This variable
369
+ # is then used to compute the value of the computed_field `pdf_file`.
370
+ # if there is a process variable called pdf_file, then this variable is ignored (i guess)
371
+ pdf_file_var_name: str
372
+ png_file_var_name: str | None = None
373
+
374
+ @computed_field
375
+ @cached_property
376
+ def pdf_file(self) -> FileInfo:
377
+ return get_file_info_indirect(self, self.pdf_file_var, required=True)
378
+
379
+ @computed_field
380
+ @cached_property
381
+ def png_file(self) -> FileInfo | None:
382
+ return get_file_info_indirect(self, self.png_file_var, required=False)
383
+
384
+ @model_validator(mode='after')
385
+ def check_png_file(self):
386
+ """
387
+ Checks if the file_info is present at the given key
388
+ """
389
+ # this is a way to compute the value of the computed_field on initialization
390
+ # to trigger validation immediately
391
+ _ = self.png_file
392
+ return self
393
+
394
+ @model_validator(mode='after')
395
+ def check_pdf_file(self):
396
+ """
397
+ Checks if the file_info is present at the given key
398
+ """
399
+ # this is a way to compute the value of the computed_field on initialization
400
+ # to trigger validation immediately
401
+ _ = self.pdf_file
402
+ return self
403
+
404
+
405
+ class Option3InputModel(BaseInputModel):
406
+ """
407
+ This is the third option to deal with file variables.
408
+
409
+ The variable names are not known and are subject to change. But there is a list of
410
+ variable names (type string) that hold the names of the file variables.
411
+ """
412
+ file_var_names: list[str]
413
+
414
+ @computed_field
415
+ @cached_property
416
+ def files(self) -> list[FileInfo]:
417
+ return [get_file_info_indirect(self, file_var_name, required=True) for file_var_name in self.file_var_names]
418
+
419
+ @model_validator(mode='after')
420
+ def check_files(self):
421
+ """
422
+ Checks if the file_info is present at the given key
423
+ """
424
+ # this is a way to compute the value of the computed_field on initialization
425
+ # to trigger validation immediately
426
+ _ = self.files
427
+ return self
428
+
429
+
430
+ @process_a.external_task("option1")
431
+ async def option1(ctx: Context, input_data: Option1InputModel) -> None:
432
+ """
433
+ This is the first option to deal with file variables.
434
+
435
+ The variable names are known and can be used directly, that means there is a process
436
+ variable called `pdf_file_var` and `png_file_var` in camunda with type file.
437
+ """
438
+ pdf_file = await ctx.download_file(input_data.pdf_file)
439
+ if input_data.png_file:
440
+ png_file = await ctx.download_file(input_data.png_file)
441
+ # Or use gather ...
442
+
443
+ # ... do something with the files ...
444
+ return
445
+
446
+
447
+ @process_a.external_task("option2")
448
+ async def option2(ctx: Context, input_data: Option2InputModel) -> None:
449
+ """
450
+ This is the second option to deal with file variables.
451
+
452
+ See that implementation of the external task is the same as with option 1,
453
+ the difference is just that the variable names in camunda might differ as long
454
+ as there are string variables telling you the names of the file variables.
455
+ """
456
+ pdf_file = await ctx.download_file(input_data.pdf_file)
457
+ if input_data.png_file:
458
+ png_file = await ctx.download_file(input_data.png_file)
459
+ # Or use gather ...
460
+
461
+ # ... do something with the files ...
462
+ return
463
+
464
+
465
+ @process_a.external_task("option3")
466
+ async def option3(ctx: Context, input_data: Option3InputModel) -> None:
467
+ """
468
+ This is the third option to deal with file variables.
469
+
470
+ See that implementation of the external task is the same as with option 1,
471
+ the difference is just that the variable names in camunda might differ as long
472
+ as there are string variables telling you the names of the file variables.
473
+ """
474
+ files = await asyncio.gather(*[ctx.download_file(file) for file in input_data.files])
475
+ # ... do something with the files ... (the caveat is that you need to know the order and meaning of the files)
476
+ # but there might be use cases where this won't matter).
477
+ return
478
+
479
+ ```
480
+
481
+
482
+
483
+ ## Development
484
+
485
+ ```shell
486
+ # setup evn ...
487
+ uv sync
488
+
489
+ # install commit hooks
490
+ prek install
491
+ ```