ergon-framework-python 0.2.2__tar.gz → 0.2.4__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 (131) hide show
  1. {ergon_framework_python-0.2.2/src/ergon_framework_python.egg-info → ergon_framework_python-0.2.4}/PKG-INFO +47 -3
  2. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/README.md +42 -2
  3. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/pyproject.toml +6 -1
  4. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/__init__.py +1 -1
  5. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/__init__.py +54 -0
  6. ergon_framework_python-0.2.4/src/ergon/connector/outlook/__init__.py +67 -0
  7. ergon_framework_python-0.2.4/src/ergon/connector/outlook/async_connector.py +198 -0
  8. ergon_framework_python-0.2.4/src/ergon/connector/outlook/async_service.py +168 -0
  9. ergon_framework_python-0.2.4/src/ergon/connector/outlook/connector.py +198 -0
  10. ergon_framework_python-0.2.4/src/ergon/connector/outlook/models.py +315 -0
  11. ergon_framework_python-0.2.4/src/ergon/connector/outlook/service.py +410 -0
  12. ergon_framework_python-0.2.4/src/ergon/connector/outlook/utils.py +118 -0
  13. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/async_service.py +61 -18
  14. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/mixins/consumer.py +34 -22
  15. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4/src/ergon_framework_python.egg-info}/PKG-INFO +47 -3
  16. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon_framework_python.egg-info/SOURCES.txt +7 -0
  17. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon_framework_python.egg-info/requires.txt +5 -0
  18. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/tests/test_optional_connectors.py +40 -1
  19. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/tests/test_smoke.py +1 -1
  20. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/LICENSE +0 -0
  21. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/setup.cfg +0 -0
  22. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/__init__.py +0 -0
  23. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/_observability/docker-compose.telemetry.yml +0 -0
  24. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/_observability/grafana.yaml +0 -0
  25. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/_observability/loki.yaml +0 -0
  26. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/_observability/otel-collector-config.yaml +0 -0
  27. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/_observability/prometheus.yaml +0 -0
  28. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/_observability/tempo.yaml +0 -0
  29. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/connectors/__init__.py +0 -0
  30. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/main.py +0 -0
  31. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/__init__.py +0 -0
  32. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/constants.py +0 -0
  33. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/example_task/__init__.py +0 -0
  34. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/example_task/config.py +0 -0
  35. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/example_task/exceptions.py +0 -0
  36. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/example_task/helpers.py +0 -0
  37. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/example_task/schemas.py +0 -0
  38. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/example_task/task.py +0 -0
  39. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/exceptions.py +0 -0
  40. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/helpers.py +0 -0
  41. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/schemas.py +0 -0
  42. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/bootstrap/src/__project__/tasks/settings.py +0 -0
  43. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/cli.py +0 -0
  44. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/_lazy.py +0 -0
  45. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/connector.py +0 -0
  46. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/__init__.py +0 -0
  47. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/_client.py +0 -0
  48. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/async_connector.py +0 -0
  49. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/__init__.py +0 -0
  50. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/adapters.py +0 -0
  51. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/async_connector.py +0 -0
  52. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/connector.py +0 -0
  53. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/models.py +0 -0
  54. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/__init__.py +0 -0
  55. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/activity.py +0 -0
  56. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/addresses.py +0 -0
  57. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/attachments.py +0 -0
  58. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/messages.py +0 -0
  59. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/records.py +0 -0
  60. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/channels/services/service.py +0 -0
  61. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/connector.py +0 -0
  62. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/models.py +0 -0
  63. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/utils.py +0 -0
  64. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/workflows/__init__.py +0 -0
  65. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/workflows/_operations.py +0 -0
  66. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/workflows/async_connector.py +0 -0
  67. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/workflows/connector.py +0 -0
  68. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/workflows/models.py +0 -0
  69. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/ergon_platform/workflows/utils.py +0 -0
  70. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/excel/__init__.py +0 -0
  71. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/excel/connector.py +0 -0
  72. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/excel/models.py +0 -0
  73. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/excel/service.py +0 -0
  74. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/__init__.py +0 -0
  75. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/async_auth_service.py +0 -0
  76. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/async_connector.py +0 -0
  77. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/async_service.py +0 -0
  78. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/auth_service.py +0 -0
  79. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/connector.py +0 -0
  80. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/models.py +0 -0
  81. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/service.py +0 -0
  82. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/nylas/utils.py +0 -0
  83. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/__init__.py +0 -0
  84. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/async_connector.py +0 -0
  85. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/async_service.py +0 -0
  86. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/connector.py +0 -0
  87. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/models.py +0 -0
  88. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/service.py +0 -0
  89. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/pipefy/version.py +0 -0
  90. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/postgres/__init__.py +0 -0
  91. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/postgres/async_connector.py +0 -0
  92. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/postgres/async_service.py +0 -0
  93. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/postgres/models.py +0 -0
  94. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/__init__.py +0 -0
  95. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/async_connector.py +0 -0
  96. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/connector.py +0 -0
  97. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/helper.py +0 -0
  98. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/models.py +0 -0
  99. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/rabbitmq/service.py +0 -0
  100. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/sqs/__init__.py +0 -0
  101. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/sqs/async_connector.py +0 -0
  102. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/sqs/async_service.py +0 -0
  103. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/sqs/connector.py +0 -0
  104. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/sqs/models.py +0 -0
  105. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/sqs/service.py +0 -0
  106. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/connector/transaction.py +0 -0
  107. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/py.typed +0 -0
  108. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/service/__init__.py +0 -0
  109. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/service/service.py +0 -0
  110. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/__init__.py +0 -0
  111. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/base.py +0 -0
  112. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/exceptions.py +0 -0
  113. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/helpers.py +0 -0
  114. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/liveness.py +0 -0
  115. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/manager.py +0 -0
  116. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/mixins/__init__.py +0 -0
  117. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/mixins/metrics.py +0 -0
  118. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/mixins/producer.py +0 -0
  119. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/policies.py +0 -0
  120. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/runner.py +0 -0
  121. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/task/utils.py +0 -0
  122. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/telemetry/__init__.py +0 -0
  123. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/telemetry/_resource.py +0 -0
  124. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/telemetry/logging.py +0 -0
  125. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/telemetry/metrics.py +0 -0
  126. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/telemetry/tracing.py +0 -0
  127. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/utils/__init__.py +0 -0
  128. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon/utils/env.py +0 -0
  129. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon_framework_python.egg-info/dependency_links.txt +0 -0
  130. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon_framework_python.egg-info/entry_points.txt +0 -0
  131. {ergon_framework_python-0.2.2 → ergon_framework_python-0.2.4}/src/ergon_framework_python.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ergon-framework-python
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Ergon internal task-oriented project bootstrapper
5
5
  Author-email: Ergondata Technologies <anza.vossos@protonmail.com>
6
6
  License-Expression: MIT
@@ -31,6 +31,7 @@ Requires-Dist: asyncpg>=0.29.0; extra == "all"
31
31
  Requires-Dist: boto3>=1.35.0; extra == "all"
32
32
  Requires-Dist: ergon-platform-sdk>=0.2.0; extra == "all"
33
33
  Requires-Dist: httpx>=0.28.0; extra == "all"
34
+ Requires-Dist: msal>=1.37.0; extra == "all"
34
35
  Requires-Dist: nylas>=6.0.0; extra == "all"
35
36
  Requires-Dist: openpyxl>=3.1.5; extra == "all"
36
37
  Requires-Dist: pika>=1.3.0; extra == "all"
@@ -42,6 +43,9 @@ Provides-Extra: excel
42
43
  Requires-Dist: openpyxl>=3.1.5; extra == "excel"
43
44
  Provides-Extra: nylas
44
45
  Requires-Dist: nylas>=6.0.0; extra == "nylas"
46
+ Provides-Extra: outlook
47
+ Requires-Dist: msal>=1.37.0; extra == "outlook"
48
+ Requires-Dist: requests>=2.32.5; extra == "outlook"
45
49
  Provides-Extra: pipefy
46
50
  Requires-Dist: httpx>=0.28.0; extra == "pipefy"
47
51
  Requires-Dist: requests>=2.32.5; extra == "pipefy"
@@ -77,13 +81,53 @@ pull in client libraries that your application does not use:
77
81
  ```bash
78
82
  pip install 'ergon-framework-python[rabbitmq]'
79
83
  pip install 'ergon-framework-python[sqs,postgres]'
84
+ pip install 'ergon-framework-python[outlook]'
80
85
  pip install 'ergon-framework-python[all]'
81
86
  ```
82
87
 
83
88
  Available extras are `rabbitmq`, `sqs`, `postgres`, `pipefy`, `excel`,
84
- `ergon-platform`, and `nylas`.
89
+ `ergon-platform`, `nylas`, and `outlook`.
85
90
 
86
- For local development, install in editable mode:
91
+ ### Outlook / Microsoft Graph
92
+
93
+ ```bash
94
+ pip install 'ergon-framework-python[outlook]'
95
+ ```
96
+
97
+ The Outlook connector talks to Microsoft Graph with Entra **application**
98
+ credentials. Consuming messages and attachments needs `Mail.Read`; marking,
99
+ categorizing, moving, and deleting need `Mail.ReadWrite`; send, reply, and
100
+ forward need `Mail.Send`. Graph `$filter` and `$search` cannot be combined.
101
+
102
+ ```python
103
+ from ergon.connector.outlook import (
104
+ AsyncOutlookGraphConnector,
105
+ OutlookAckActionConfig,
106
+ OutlookConsumerConfig,
107
+ OutlookGraphClient,
108
+ OutlookMessageFilter,
109
+ )
110
+
111
+ client = OutlookGraphClient(
112
+ tenant_id="...",
113
+ client_id="...",
114
+ client_secret="...",
115
+ user_email="mailbox@example.com",
116
+ )
117
+ consumer = OutlookConsumerConfig(
118
+ folder_id="Inbox",
119
+ filter=OutlookMessageFilter(unread_only=True, has_attachments=True),
120
+ download_attachments=True,
121
+ ack_config=OutlookAckActionConfig(mark_as_read=True),
122
+ )
123
+ ```
124
+
125
+ Runnable example: [examples/outlook](https://github.com/danzanellovossos/ergon-framework/tree/main/sdks/python/examples/outlook).
126
+ Module reference: [Outlook connector README](https://github.com/danzanellovossos/ergon-framework/blob/main/sdks/python/src/ergon/connector/outlook/README.md).
127
+
128
+ ### Local development
129
+
130
+ Install in editable mode:
87
131
 
88
132
  ```bash
89
133
  pip install -e /path/to/ergon-framework/sdks/python
@@ -20,13 +20,53 @@ pull in client libraries that your application does not use:
20
20
  ```bash
21
21
  pip install 'ergon-framework-python[rabbitmq]'
22
22
  pip install 'ergon-framework-python[sqs,postgres]'
23
+ pip install 'ergon-framework-python[outlook]'
23
24
  pip install 'ergon-framework-python[all]'
24
25
  ```
25
26
 
26
27
  Available extras are `rabbitmq`, `sqs`, `postgres`, `pipefy`, `excel`,
27
- `ergon-platform`, and `nylas`.
28
+ `ergon-platform`, `nylas`, and `outlook`.
28
29
 
29
- For local development, install in editable mode:
30
+ ### Outlook / Microsoft Graph
31
+
32
+ ```bash
33
+ pip install 'ergon-framework-python[outlook]'
34
+ ```
35
+
36
+ The Outlook connector talks to Microsoft Graph with Entra **application**
37
+ credentials. Consuming messages and attachments needs `Mail.Read`; marking,
38
+ categorizing, moving, and deleting need `Mail.ReadWrite`; send, reply, and
39
+ forward need `Mail.Send`. Graph `$filter` and `$search` cannot be combined.
40
+
41
+ ```python
42
+ from ergon.connector.outlook import (
43
+ AsyncOutlookGraphConnector,
44
+ OutlookAckActionConfig,
45
+ OutlookConsumerConfig,
46
+ OutlookGraphClient,
47
+ OutlookMessageFilter,
48
+ )
49
+
50
+ client = OutlookGraphClient(
51
+ tenant_id="...",
52
+ client_id="...",
53
+ client_secret="...",
54
+ user_email="mailbox@example.com",
55
+ )
56
+ consumer = OutlookConsumerConfig(
57
+ folder_id="Inbox",
58
+ filter=OutlookMessageFilter(unread_only=True, has_attachments=True),
59
+ download_attachments=True,
60
+ ack_config=OutlookAckActionConfig(mark_as_read=True),
61
+ )
62
+ ```
63
+
64
+ Runnable example: [examples/outlook](https://github.com/danzanellovossos/ergon-framework/tree/main/sdks/python/examples/outlook).
65
+ Module reference: [Outlook connector README](https://github.com/danzanellovossos/ergon-framework/blob/main/sdks/python/src/ergon/connector/outlook/README.md).
66
+
67
+ ### Local development
68
+
69
+ Install in editable mode:
30
70
 
31
71
  ```bash
32
72
  pip install -e /path/to/ergon-framework/sdks/python
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ergon-framework-python"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "Ergon internal task-oriented project bootstrapper"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -36,6 +36,7 @@ all = [
36
36
  "boto3>=1.35.0",
37
37
  "ergon-platform-sdk>=0.2.0",
38
38
  "httpx>=0.28.0",
39
+ "msal>=1.37.0",
39
40
  "nylas>=6.0.0",
40
41
  "openpyxl>=3.1.5",
41
42
  "pika>=1.3.0",
@@ -47,6 +48,10 @@ all = [
47
48
  ]
48
49
  excel = ["openpyxl>=3.1.5"]
49
50
  nylas = ["nylas>=6.0.0"]
51
+ outlook = [
52
+ "msal>=1.37.0",
53
+ "requests>=2.32.5",
54
+ ]
50
55
  pipefy = [
51
56
  "httpx>=0.28.0",
52
57
  "requests>=2.32.5",
@@ -10,4 +10,4 @@ __all__ = [
10
10
  "manager",
11
11
  ]
12
12
 
13
- __version__ = "0.2.2"
13
+ __version__ = "0.2.4"
@@ -43,6 +43,25 @@ if TYPE_CHECKING:
43
43
  NylasService,
44
44
  SendMessageInput,
45
45
  )
46
+ from .outlook import (
47
+ AsyncOutlookGraphConnector,
48
+ AsyncOutlookGraphService,
49
+ OutlookAckActionConfig,
50
+ OutlookAttachmentInput,
51
+ OutlookConsumerConfig,
52
+ OutlookEmailAddress,
53
+ OutlookFlagStatus,
54
+ OutlookGraphClient,
55
+ OutlookGraphConnector,
56
+ OutlookGraphService,
57
+ OutlookMessageFilter,
58
+ OutlookMessageQuery,
59
+ OutlookMessageSearch,
60
+ OutlookNackActionConfig,
61
+ OutlookProducerConfig,
62
+ OutlookSendMessageInput,
63
+ OutlookWellKnownFolder,
64
+ )
46
65
  from .postgres import (
47
66
  AsyncPostgresConnector,
48
67
  AsyncPostgresService,
@@ -78,6 +97,7 @@ _CONNECTOR_SUBPACKAGES = (
78
97
  "ergon_platform",
79
98
  "excel",
80
99
  "nylas",
100
+ "outlook",
81
101
  "pipefy",
82
102
  "postgres",
83
103
  "rabbitmq",
@@ -91,6 +111,8 @@ _LAZY_EXPORTS = {
91
111
  "AsyncNylasAuthService": "nylas",
92
112
  "AsyncNylasConnector": "nylas",
93
113
  "AsyncNylasService": "nylas",
114
+ "AsyncOutlookGraphConnector": "outlook",
115
+ "AsyncOutlookGraphService": "outlook",
94
116
  "AsyncPostgresConnector": "postgres",
95
117
  "AsyncPostgresService": "postgres",
96
118
  "AsyncRabbitMQConnector": "rabbitmq",
@@ -126,6 +148,21 @@ _LAZY_EXPORTS = {
126
148
  "NylasConsumerConfig": "nylas",
127
149
  "NylasProducerConfig": "nylas",
128
150
  "NylasService": "nylas",
151
+ "OutlookAckActionConfig": "outlook",
152
+ "OutlookAttachmentInput": "outlook",
153
+ "OutlookConsumerConfig": "outlook",
154
+ "OutlookEmailAddress": "outlook",
155
+ "OutlookFlagStatus": "outlook",
156
+ "OutlookGraphClient": "outlook",
157
+ "OutlookGraphConnector": "outlook",
158
+ "OutlookGraphService": "outlook",
159
+ "OutlookMessageFilter": "outlook",
160
+ "OutlookMessageQuery": "outlook",
161
+ "OutlookMessageSearch": "outlook",
162
+ "OutlookNackActionConfig": "outlook",
163
+ "OutlookProducerConfig": "outlook",
164
+ "OutlookSendMessageInput": "outlook",
165
+ "OutlookWellKnownFolder": "outlook",
129
166
  "PostgresClient": "postgres",
130
167
  "PostgresConsumerConfig": "postgres",
131
168
  "PostgresProducerConfig": "postgres",
@@ -168,6 +205,8 @@ __all__ = [
168
205
  "AsyncNylasAuthService",
169
206
  "AsyncNylasConnector",
170
207
  "AsyncNylasService",
208
+ "AsyncOutlookGraphConnector",
209
+ "AsyncOutlookGraphService",
171
210
  "AuthUrlConfig",
172
211
  "CodeExchangeInput",
173
212
  "GrantAuthResult",
@@ -205,6 +244,21 @@ __all__ = [
205
244
  "NylasConsumerConfig",
206
245
  "NylasProducerConfig",
207
246
  "NylasService",
247
+ "OutlookAckActionConfig",
248
+ "OutlookAttachmentInput",
249
+ "OutlookConsumerConfig",
250
+ "OutlookEmailAddress",
251
+ "OutlookFlagStatus",
252
+ "OutlookGraphClient",
253
+ "OutlookGraphConnector",
254
+ "OutlookGraphService",
255
+ "OutlookMessageFilter",
256
+ "OutlookMessageQuery",
257
+ "OutlookMessageSearch",
258
+ "OutlookNackActionConfig",
259
+ "OutlookProducerConfig",
260
+ "OutlookSendMessageInput",
261
+ "OutlookWellKnownFolder",
208
262
  "PostgresClient",
209
263
  "SendMessageInput",
210
264
  "PostgresConsumerConfig",
@@ -0,0 +1,67 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from .._lazy import exported_names, load_optional_export
4
+ from .models import (
5
+ OutlookAckActionConfig,
6
+ OutlookAttachmentInput,
7
+ OutlookConsumerConfig,
8
+ OutlookEmailAddress,
9
+ OutlookFlagStatus,
10
+ OutlookGraphClient,
11
+ OutlookMessageFilter,
12
+ OutlookMessageQuery,
13
+ OutlookMessageSearch,
14
+ OutlookNackActionConfig,
15
+ OutlookProducerConfig,
16
+ OutlookSendMessageInput,
17
+ OutlookWellKnownFolder,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from .async_connector import AsyncOutlookGraphConnector
22
+ from .async_service import AsyncOutlookGraphService
23
+ from .connector import OutlookGraphConnector
24
+ from .service import OutlookGraphService
25
+
26
+ _LAZY_EXPORTS = {
27
+ "AsyncOutlookGraphConnector": "async_connector",
28
+ "AsyncOutlookGraphService": "async_service",
29
+ "OutlookGraphConnector": "connector",
30
+ "OutlookGraphService": "service",
31
+ }
32
+
33
+
34
+ def __getattr__(name: str) -> Any:
35
+ return load_optional_export(
36
+ name=name,
37
+ package=__name__,
38
+ exports=_LAZY_EXPORTS,
39
+ namespace=globals(),
40
+ extra="outlook",
41
+ dependencies=("msal", "requests"),
42
+ )
43
+
44
+
45
+ def __dir__() -> list[str]:
46
+ return exported_names(globals(), _LAZY_EXPORTS)
47
+
48
+
49
+ __all__ = [
50
+ "AsyncOutlookGraphConnector",
51
+ "AsyncOutlookGraphService",
52
+ "OutlookAckActionConfig",
53
+ "OutlookAttachmentInput",
54
+ "OutlookConsumerConfig",
55
+ "OutlookEmailAddress",
56
+ "OutlookFlagStatus",
57
+ "OutlookGraphClient",
58
+ "OutlookGraphConnector",
59
+ "OutlookGraphService",
60
+ "OutlookMessageFilter",
61
+ "OutlookMessageQuery",
62
+ "OutlookMessageSearch",
63
+ "OutlookNackActionConfig",
64
+ "OutlookProducerConfig",
65
+ "OutlookSendMessageInput",
66
+ "OutlookWellKnownFolder",
67
+ ]
@@ -0,0 +1,198 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from ..connector import AsyncConnector
6
+ from ..transaction import Transaction
7
+ from .async_service import AsyncOutlookGraphService
8
+ from .models import (
9
+ OutlookAckActionConfig,
10
+ OutlookConsumerConfig,
11
+ OutlookEmailAddress,
12
+ OutlookFlagStatus,
13
+ OutlookGraphClient,
14
+ OutlookMessageQuery,
15
+ OutlookNackActionConfig,
16
+ OutlookProducerConfig,
17
+ OutlookSendMessagePayload,
18
+ OutlookWellKnownFolder,
19
+ )
20
+ from .utils import build_ack_patch, build_nack_patch, merge_query
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class AsyncOutlookGraphConnector(AsyncConnector):
26
+ service: AsyncOutlookGraphService
27
+
28
+ def __init__(
29
+ self,
30
+ client: OutlookGraphClient,
31
+ consumer_config: Optional[OutlookConsumerConfig] = None,
32
+ producer_config: Optional[OutlookProducerConfig] = None,
33
+ ) -> None:
34
+ self.service = AsyncOutlookGraphService(client)
35
+ self._consumer_config = consumer_config
36
+ self._producer_config = producer_config or OutlookProducerConfig()
37
+
38
+ async def fetch_transactions_async(
39
+ self,
40
+ batch_size: Optional[int] = None,
41
+ query_overrides: Optional[OutlookMessageQuery] = None,
42
+ *args: Any,
43
+ **kwargs: Any,
44
+ ) -> List[Transaction]:
45
+ if self._consumer_config is None:
46
+ raise ValueError("AsyncOutlookGraphConnector requires a consumer_config to fetch transactions")
47
+ config = self._consumer_config
48
+ return await self.service.fetch_items(
49
+ merge_query(config, query_overrides, **kwargs),
50
+ batch_size or config.batch_size,
51
+ download_attachments=config.download_attachments,
52
+ )
53
+
54
+ async def dispatch_transactions_async(
55
+ self,
56
+ transactions: List[Transaction],
57
+ *args: Any,
58
+ **kwargs: Any,
59
+ ) -> List[str]:
60
+ dispatched_ids: List[str] = []
61
+ for transaction in transactions:
62
+ await self.service.send_message(
63
+ transaction.payload,
64
+ producer_config=self._producer_config,
65
+ )
66
+ dispatched_ids.append(transaction.id)
67
+ return dispatched_ids
68
+
69
+ async def send_message(self, payload: OutlookSendMessagePayload) -> None:
70
+ await self.service.send_message(payload, producer_config=self._producer_config)
71
+
72
+ async def mark_as_read(self, message_id: str) -> Dict[str, Any]:
73
+ return await self.service.mark_as_read(message_id)
74
+
75
+ async def mark_as_unread(self, message_id: str) -> Dict[str, Any]:
76
+ return await self.service.mark_as_unread(message_id)
77
+
78
+ async def set_flag(
79
+ self,
80
+ message_id: str,
81
+ status: OutlookFlagStatus | str = OutlookFlagStatus.FLAGGED,
82
+ ) -> Dict[str, Any]:
83
+ return await self.service.set_flag(message_id, status)
84
+
85
+ async def set_categories(self, message_id: str, categories: List[str]) -> Dict[str, Any]:
86
+ return await self.service.set_categories(message_id, categories)
87
+
88
+ async def move_message(
89
+ self,
90
+ message_id: str,
91
+ folder_id: OutlookWellKnownFolder | str,
92
+ ) -> Dict[str, Any]:
93
+ return await self.service.move_message(message_id, folder_id)
94
+
95
+ async def delete_message(self, message_id: str) -> None:
96
+ await self.service.delete_message(message_id)
97
+
98
+ async def reply(self, message_id: str, *, comment: str = "") -> None:
99
+ await self.service.reply(message_id, comment=comment)
100
+
101
+ async def reply_all(self, message_id: str, *, comment: str = "") -> None:
102
+ await self.service.reply_all(message_id, comment=comment)
103
+
104
+ async def forward(
105
+ self,
106
+ message_id: str,
107
+ to: List[OutlookEmailAddress],
108
+ comment: str = "",
109
+ ) -> None:
110
+ await self.service.forward(message_id, to, comment=comment)
111
+
112
+ async def list_attachments(
113
+ self,
114
+ message_id: str,
115
+ download_content: bool = False,
116
+ ) -> List[Dict[str, Any]]:
117
+ return await self.service.list_attachments(message_id, download_content=download_content)
118
+
119
+ async def save_attachment(
120
+ self,
121
+ attachment: Dict[str, Any],
122
+ destination: str | Path,
123
+ overwrite: bool = False,
124
+ ) -> Path:
125
+ return await self.service.save_attachment(attachment, destination, overwrite=overwrite)
126
+
127
+ async def list_mail_folders(self, include_hidden: bool = False) -> List[Dict[str, Any]]:
128
+ return await self.service.list_mail_folders(include_hidden=include_hidden)
129
+
130
+ async def get_mail_folder(self, folder_id: OutlookWellKnownFolder | str) -> Dict[str, Any]:
131
+ return await self.service.get_mail_folder(folder_id)
132
+
133
+ async def reset_pagination(self) -> None:
134
+ await self.service.reset_pagination()
135
+
136
+ async def fetch_transaction_by_id_async(
137
+ self,
138
+ transaction_id: str,
139
+ *args: Any,
140
+ **kwargs: Any,
141
+ ) -> Transaction:
142
+ config = self._consumer_config
143
+ return await self.service.find_message_transaction(
144
+ transaction_id,
145
+ select=kwargs.get("select"),
146
+ download_attachments=config.download_attachments if config else False,
147
+ )
148
+
149
+ async def get_transactions_count_async(self, *args: Any, **kwargs: Any) -> int:
150
+ if self._consumer_config is None:
151
+ raise ValueError("AsyncOutlookGraphConnector requires a consumer_config to count transactions")
152
+ query_overrides = kwargs.pop("query_overrides", None)
153
+ max_pages = kwargs.pop("max_pages", None)
154
+ query = merge_query(self._consumer_config, query_overrides, **kwargs)
155
+ return await self.service.get_messages_count(query, max_pages=max_pages)
156
+
157
+ async def ack_transaction(
158
+ self,
159
+ transaction: Transaction,
160
+ ack_config: Optional[OutlookAckActionConfig] = None,
161
+ ) -> None:
162
+ config = ack_config or (self._consumer_config.ack_config if self._consumer_config else None)
163
+ if config is None:
164
+ return
165
+ if config.delete:
166
+ await self.service.delete_message(transaction.id)
167
+ return
168
+ patch = build_ack_patch(config)
169
+ if patch:
170
+ await self.service.update_message(transaction.id, patch)
171
+ if config.move_to_folder_id:
172
+ await self.service.move_message(transaction.id, config.move_to_folder_id)
173
+
174
+ async def nack_transaction(
175
+ self,
176
+ transaction: Transaction,
177
+ requeue: bool = True,
178
+ nack_config: Optional[OutlookNackActionConfig] = None,
179
+ ) -> None:
180
+ config = nack_config or (self._consumer_config.nack_config if self._consumer_config else None)
181
+ if config is None:
182
+ if not requeue:
183
+ raise ValueError("nack_config with a failure category or folder is required when requeue=False")
184
+ config = OutlookNackActionConfig()
185
+ if not requeue and not config.move_to_folder_id and not config.categories:
186
+ raise ValueError("nack_config with a failure category or folder is required when requeue=False")
187
+
188
+ patch = build_nack_patch(config)
189
+ if patch:
190
+ await self.service.update_message(transaction.id, patch)
191
+ if config.move_to_folder_id:
192
+ await self.service.move_message(transaction.id, config.move_to_folder_id)
193
+ if requeue:
194
+ await self.service.reset_pagination()
195
+ logger.debug("Outlook message %s was made available for refetch", transaction.id)
196
+
197
+ async def close(self) -> None:
198
+ await self.service.close()
@@ -0,0 +1,168 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ from ..transaction import Transaction
6
+ from .models import (
7
+ OutlookEmailAddress,
8
+ OutlookFlagStatus,
9
+ OutlookGraphClient,
10
+ OutlookMessageQuery,
11
+ OutlookProducerConfig,
12
+ OutlookSendMessagePayload,
13
+ OutlookWellKnownFolder,
14
+ )
15
+ from .service import OutlookGraphService
16
+
17
+
18
+ class AsyncOutlookGraphService:
19
+ """Async facade over the blocking MSAL and requests implementations."""
20
+
21
+ def __init__(self, client: OutlookGraphClient) -> None:
22
+ self._sync = OutlookGraphService(client)
23
+
24
+ @property
25
+ def user_email(self) -> str:
26
+ return self._sync.user_email
27
+
28
+ async def close(self) -> None:
29
+ await asyncio.to_thread(self._sync.close)
30
+
31
+ async def reset_pagination(self) -> None:
32
+ self._sync.reset_pagination()
33
+
34
+ async def list_messages(
35
+ self,
36
+ query: OutlookMessageQuery,
37
+ limit: int,
38
+ next_link: Optional[str] = None,
39
+ use_internal_pagination: bool = True,
40
+ ) -> Dict[str, Any]:
41
+ return await asyncio.to_thread(
42
+ self._sync.list_messages,
43
+ query,
44
+ limit,
45
+ next_link=next_link,
46
+ use_internal_pagination=use_internal_pagination,
47
+ )
48
+
49
+ async def find_message(self, message_id: str, *, select: Optional[List[str]] = None) -> Dict[str, Any]:
50
+ return await asyncio.to_thread(self._sync.find_message, message_id, select=select)
51
+
52
+ async def list_attachments(
53
+ self,
54
+ message_id: str,
55
+ download_content: bool = False,
56
+ ) -> List[Dict[str, Any]]:
57
+ return await asyncio.to_thread(
58
+ self._sync.list_attachments,
59
+ message_id,
60
+ download_content=download_content,
61
+ )
62
+
63
+ async def save_attachment(
64
+ self,
65
+ attachment: Dict[str, Any],
66
+ destination: str | Path,
67
+ overwrite: bool = False,
68
+ ) -> Path:
69
+ return await asyncio.to_thread(
70
+ self._sync.save_attachment,
71
+ attachment,
72
+ destination,
73
+ overwrite=overwrite,
74
+ )
75
+
76
+ async def fetch_items(
77
+ self,
78
+ query: OutlookMessageQuery,
79
+ limit: int,
80
+ download_attachments: bool = False,
81
+ ) -> List[Transaction]:
82
+ return await asyncio.to_thread(
83
+ self._sync.fetch_items,
84
+ query,
85
+ limit,
86
+ download_attachments=download_attachments,
87
+ )
88
+
89
+ async def find_message_transaction(
90
+ self,
91
+ message_id: str,
92
+ select: Optional[List[str]] = None,
93
+ download_attachments: bool = False,
94
+ ) -> Transaction:
95
+ return await asyncio.to_thread(
96
+ self._sync.find_message_transaction,
97
+ message_id,
98
+ select=select,
99
+ download_attachments=download_attachments,
100
+ )
101
+
102
+ async def get_messages_count(
103
+ self,
104
+ query: OutlookMessageQuery,
105
+ max_pages: Optional[int] = None,
106
+ ) -> int:
107
+ return await asyncio.to_thread(self._sync.get_messages_count, query, max_pages=max_pages)
108
+
109
+ async def send_message(
110
+ self,
111
+ payload: OutlookSendMessagePayload,
112
+ producer_config: Optional[OutlookProducerConfig] = None,
113
+ ) -> None:
114
+ await asyncio.to_thread(
115
+ self._sync.send_message,
116
+ payload,
117
+ producer_config=producer_config,
118
+ )
119
+
120
+ async def update_message(self, message_id: str, request_body: Dict[str, Any]) -> Dict[str, Any]:
121
+ return await asyncio.to_thread(self._sync.update_message, message_id, request_body)
122
+
123
+ async def mark_as_read(self, message_id: str) -> Dict[str, Any]:
124
+ return await asyncio.to_thread(self._sync.mark_as_read, message_id)
125
+
126
+ async def mark_as_unread(self, message_id: str) -> Dict[str, Any]:
127
+ return await asyncio.to_thread(self._sync.mark_as_unread, message_id)
128
+
129
+ async def set_flag(
130
+ self,
131
+ message_id: str,
132
+ status: OutlookFlagStatus | str = OutlookFlagStatus.FLAGGED,
133
+ ) -> Dict[str, Any]:
134
+ return await asyncio.to_thread(self._sync.set_flag, message_id, status)
135
+
136
+ async def set_categories(self, message_id: str, categories: List[str]) -> Dict[str, Any]:
137
+ return await asyncio.to_thread(self._sync.set_categories, message_id, categories)
138
+
139
+ async def move_message(
140
+ self,
141
+ message_id: str,
142
+ folder_id: OutlookWellKnownFolder | str,
143
+ ) -> Dict[str, Any]:
144
+ return await asyncio.to_thread(self._sync.move_message, message_id, folder_id)
145
+
146
+ async def delete_message(self, message_id: str) -> None:
147
+ await asyncio.to_thread(self._sync.delete_message, message_id)
148
+
149
+ async def reply(self, message_id: str, *, comment: str = "") -> None:
150
+ await asyncio.to_thread(self._sync.reply, message_id, comment=comment)
151
+
152
+ async def reply_all(self, message_id: str, *, comment: str = "") -> None:
153
+ await asyncio.to_thread(self._sync.reply_all, message_id, comment=comment)
154
+
155
+ async def forward(
156
+ self,
157
+ message_id: str,
158
+ to: List[OutlookEmailAddress],
159
+ *,
160
+ comment: str = "",
161
+ ) -> None:
162
+ await asyncio.to_thread(self._sync.forward, message_id, to, comment=comment)
163
+
164
+ async def list_mail_folders(self, *, include_hidden: bool = False) -> List[Dict[str, Any]]:
165
+ return await asyncio.to_thread(self._sync.list_mail_folders, include_hidden=include_hidden)
166
+
167
+ async def get_mail_folder(self, folder_id: OutlookWellKnownFolder | str) -> Dict[str, Any]:
168
+ return await asyncio.to_thread(self._sync.get_mail_folder, folder_id)