trustgraph-base 2.3.14__tar.gz → 2.3.16__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 (180) hide show
  1. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/PKG-INFO +2 -1
  2. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/pyproject.toml +1 -0
  3. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/consumer.py +11 -0
  4. trustgraph_base-2.3.16/trustgraph/base/kafka_backend.py +452 -0
  5. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/pubsub.py +57 -0
  6. trustgraph_base-2.3.16/trustgraph/base_version.py +1 -0
  7. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph_base.egg-info/PKG-INFO +2 -1
  8. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph_base.egg-info/SOURCES.txt +1 -0
  9. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph_base.egg-info/requires.txt +1 -0
  10. trustgraph_base-2.3.14/trustgraph/base_version.py +0 -1
  11. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/README.md +0 -0
  12. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/setup.cfg +0 -0
  13. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/__init__.py +0 -0
  14. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/api.py +0 -0
  15. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/async_bulk_client.py +0 -0
  16. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/async_flow.py +0 -0
  17. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/async_metrics.py +0 -0
  18. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/async_socket_client.py +0 -0
  19. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/bulk_client.py +0 -0
  20. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/collection.py +0 -0
  21. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/config.py +0 -0
  22. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/exceptions.py +0 -0
  23. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/explainability.py +0 -0
  24. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/flow.py +0 -0
  25. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/knowledge.py +0 -0
  26. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/library.py +0 -0
  27. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/metrics.py +0 -0
  28. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/socket_client.py +0 -0
  29. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/api/types.py +0 -0
  30. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/__init__.py +0 -0
  31. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/agent_client.py +0 -0
  32. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/agent_service.py +0 -0
  33. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/async_processor.py +0 -0
  34. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/backend.py +0 -0
  35. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/cassandra_config.py +0 -0
  36. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/chunking_service.py +0 -0
  37. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/collection_config_handler.py +0 -0
  38. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/config_client.py +0 -0
  39. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/consumer_spec.py +0 -0
  40. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/document_embeddings_client.py +0 -0
  41. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/document_embeddings_query_service.py +0 -0
  42. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/document_embeddings_store_service.py +0 -0
  43. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/dynamic_tool_service.py +0 -0
  44. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/embeddings_client.py +0 -0
  45. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/embeddings_service.py +0 -0
  46. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/flow.py +0 -0
  47. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/flow_processor.py +0 -0
  48. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/graph_embeddings_client.py +0 -0
  49. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/graph_embeddings_query_service.py +0 -0
  50. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/graph_embeddings_store_service.py +0 -0
  51. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/graph_rag_client.py +0 -0
  52. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/librarian_client.py +0 -0
  53. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/llm_service.py +0 -0
  54. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/logging.py +0 -0
  55. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/metrics.py +0 -0
  56. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/parameter_spec.py +0 -0
  57. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/processor_group.py +0 -0
  58. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/producer.py +0 -0
  59. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/producer_spec.py +0 -0
  60. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/prompt_client.py +0 -0
  61. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/publisher.py +0 -0
  62. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/pulsar_backend.py +0 -0
  63. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/rabbitmq_backend.py +0 -0
  64. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/request_response_spec.py +0 -0
  65. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/row_embeddings_query_client.py +0 -0
  66. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/serialization.py +0 -0
  67. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/spec.py +0 -0
  68. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/structured_query_client.py +0 -0
  69. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/subscriber.py +0 -0
  70. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/subscriber_spec.py +0 -0
  71. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/text_completion_client.py +0 -0
  72. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/tool_client.py +0 -0
  73. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/tool_service.py +0 -0
  74. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/tool_service_client.py +0 -0
  75. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/triples_client.py +0 -0
  76. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/triples_query_service.py +0 -0
  77. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/base/triples_store_service.py +0 -0
  78. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/__init__.py +0 -0
  79. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/agent_client.py +0 -0
  80. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/base.py +0 -0
  81. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/config_client.py +0 -0
  82. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/document_embeddings_client.py +0 -0
  83. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/document_rag_client.py +0 -0
  84. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/embeddings_client.py +0 -0
  85. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/graph_embeddings_client.py +0 -0
  86. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/graph_rag_client.py +0 -0
  87. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/llm_client.py +0 -0
  88. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/prompt_client.py +0 -0
  89. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/row_embeddings_client.py +0 -0
  90. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/clients/triples_query_client.py +0 -0
  91. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/exceptions.py +0 -0
  92. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/__init__.py +0 -0
  93. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/__init__.py +0 -0
  94. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/ar.json +0 -0
  95. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/en.json +0 -0
  96. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/es.json +0 -0
  97. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/he.json +0 -0
  98. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/hi.json +0 -0
  99. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/pt.json +0 -0
  100. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/ru.json +0 -0
  101. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/sw.json +0 -0
  102. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/tr.json +0 -0
  103. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/i18n/packs/zh-cn.json +0 -0
  104. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/knowledge/__init__.py +0 -0
  105. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/knowledge/defs.py +0 -0
  106. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/knowledge/document.py +0 -0
  107. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/knowledge/identifier.py +0 -0
  108. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/knowledge/organization.py +0 -0
  109. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/knowledge/publication.py +0 -0
  110. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/log_level.py +0 -0
  111. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/__init__.py +0 -0
  112. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/registry.py +0 -0
  113. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/__init__.py +0 -0
  114. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/agent.py +0 -0
  115. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/base.py +0 -0
  116. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/collection.py +0 -0
  117. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/config.py +0 -0
  118. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/diagnosis.py +0 -0
  119. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/document_loading.py +0 -0
  120. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/embeddings.py +0 -0
  121. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/embeddings_query.py +0 -0
  122. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/flow.py +0 -0
  123. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/knowledge.py +0 -0
  124. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/library.py +0 -0
  125. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/metadata.py +0 -0
  126. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/nlp_query.py +0 -0
  127. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/primitives.py +0 -0
  128. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/prompt.py +0 -0
  129. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/retrieval.py +0 -0
  130. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/rows_query.py +0 -0
  131. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/sparql_query.py +0 -0
  132. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/structured_query.py +0 -0
  133. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/text_completion.py +0 -0
  134. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/tool.py +0 -0
  135. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/messaging/translators/triples.py +0 -0
  136. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/objects/__init__.py +0 -0
  137. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/objects/field.py +0 -0
  138. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/objects/object.py +0 -0
  139. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/provenance/__init__.py +0 -0
  140. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/provenance/agent.py +0 -0
  141. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/provenance/namespaces.py +0 -0
  142. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/provenance/triples.py +0 -0
  143. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/provenance/uris.py +0 -0
  144. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/provenance/vocabulary.py +0 -0
  145. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/rdf.py +0 -0
  146. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/__init__.py +0 -0
  147. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/core/__init__.py +0 -0
  148. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/core/metadata.py +0 -0
  149. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/core/primitives.py +0 -0
  150. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/core/topic.py +0 -0
  151. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/__init__.py +0 -0
  152. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/document.py +0 -0
  153. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/embeddings.py +0 -0
  154. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/graph.py +0 -0
  155. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/knowledge.py +0 -0
  156. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/nlp.py +0 -0
  157. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/object.py +0 -0
  158. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/rows.py +0 -0
  159. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/knowledge/structured.py +0 -0
  160. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/__init__.py +0 -0
  161. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/agent.py +0 -0
  162. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/collection.py +0 -0
  163. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/config.py +0 -0
  164. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/diagnosis.py +0 -0
  165. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/flow.py +0 -0
  166. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/library.py +0 -0
  167. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/llm.py +0 -0
  168. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/lookup.py +0 -0
  169. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/nlp_query.py +0 -0
  170. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/prompt.py +0 -0
  171. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/query.py +0 -0
  172. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/retrieval.py +0 -0
  173. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/rows_query.py +0 -0
  174. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/sparql_query.py +0 -0
  175. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/storage.py +0 -0
  176. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/structured_query.py +0 -0
  177. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph/schema/services/tool_service.py +0 -0
  178. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph_base.egg-info/dependency_links.txt +0 -0
  179. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph_base.egg-info/entry_points.txt +0 -0
  180. {trustgraph_base-2.3.14 → trustgraph_base-2.3.16}/trustgraph_base.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustgraph-base
3
- Version: 2.3.14
3
+ Version: 2.3.16
4
4
  Summary: TrustGraph provides a means to run a pipeline of flexible AI processing components in a flexible means to achieve a processing pipeline.
5
5
  Author-email: "trustgraph.ai" <security@trustgraph.ai>
6
6
  Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
@@ -13,6 +13,7 @@ Requires-Dist: prometheus-client
13
13
  Requires-Dist: requests
14
14
  Requires-Dist: python-logging-loki
15
15
  Requires-Dist: pika
16
+ Requires-Dist: confluent-kafka
16
17
  Requires-Dist: pyyaml
17
18
 
18
19
  See https://trustgraph.ai/
@@ -15,6 +15,7 @@ dependencies = [
15
15
  "requests",
16
16
  "python-logging-loki",
17
17
  "pika",
18
+ "confluent-kafka",
18
19
  "pyyaml",
19
20
  ]
20
21
  classifiers = [
@@ -54,6 +54,17 @@ class Consumer:
54
54
  self.running = True
55
55
  self.consumer_task = None
56
56
 
57
+ # Kafka topics are created with 1 partition, so multiple
58
+ # consumers in the same group causes rebalance storms where
59
+ # no consumer can fetch. Cap to the backend's limit.
60
+ max_concurrency = getattr(backend, 'max_consumer_concurrency', None)
61
+ if isinstance(max_concurrency, int) and concurrency > max_concurrency:
62
+ logger.info(
63
+ f"Capping concurrency from {concurrency} to "
64
+ f"{max_concurrency} (backend limit)"
65
+ )
66
+ concurrency = max_concurrency
67
+
57
68
  self.concurrency = concurrency
58
69
 
59
70
  self.metrics = metrics
@@ -0,0 +1,452 @@
1
+ """
2
+ Kafka backend implementation for pub/sub abstraction.
3
+
4
+ Each logical topic maps to a Kafka topic. The topic name encodes
5
+ the full identity:
6
+
7
+ class:topicspace:topic -> topicspace.class.topic
8
+
9
+ Producers publish to the topic directly.
10
+ Consumers use consumer groups for competing-consumer semantics:
11
+
12
+ - flow / request: named consumer group (competing consumers)
13
+ - response / notify: unique consumer group per instance, filtering
14
+ messages by correlation ID (all subscribers see all messages)
15
+
16
+ The flow service manages topic lifecycle via AdminClient.
17
+
18
+ Architecture:
19
+ Producer --> [Kafka topic] --> Consumer Group A --> Consumer
20
+ --> Consumer Group A --> Consumer
21
+ --> Consumer Group B --> Consumer (response)
22
+ """
23
+
24
+ import asyncio
25
+ import json
26
+ import logging
27
+ import uuid
28
+ from typing import Any
29
+
30
+ from confluent_kafka import (
31
+ Producer as KafkaProducer,
32
+ Consumer as KafkaConsumer,
33
+ TopicPartition,
34
+ KafkaError,
35
+ KafkaException,
36
+ )
37
+ from confluent_kafka.admin import AdminClient, NewTopic
38
+
39
+ from .backend import PubSubBackend, BackendProducer, BackendConsumer, Message
40
+ from .serialization import dataclass_to_dict, dict_to_dataclass
41
+
42
+ logger = logging.getLogger(__name__)
43
+
44
+ # Retention defaults (milliseconds)
45
+ LONG_RETENTION_MS = 7 * 24 * 60 * 60 * 1000 # 7 days
46
+ SHORT_RETENTION_MS = 300 * 1000 # 5 minutes
47
+
48
+
49
+ class KafkaMessage:
50
+ """Wrapper for Kafka messages to match Message protocol."""
51
+
52
+ def __init__(self, msg, schema_cls):
53
+ self._msg = msg
54
+ self._schema_cls = schema_cls
55
+ self._value = None
56
+
57
+ def value(self) -> Any:
58
+ """Deserialize and return the message value as a dataclass."""
59
+ if self._value is None:
60
+ data_dict = json.loads(self._msg.value().decode('utf-8'))
61
+ self._value = dict_to_dataclass(data_dict, self._schema_cls)
62
+ return self._value
63
+
64
+ def properties(self) -> dict:
65
+ """Return message properties from Kafka headers."""
66
+ headers = self._msg.headers() or []
67
+ return {
68
+ k: v.decode('utf-8') if isinstance(v, bytes) else v
69
+ for k, v in headers
70
+ }
71
+
72
+
73
+ class KafkaBackendProducer:
74
+ """Publishes messages to a Kafka topic.
75
+
76
+ confluent-kafka Producer is thread-safe, so a single instance
77
+ can be shared across threads.
78
+ """
79
+
80
+ def __init__(self, bootstrap_servers, topic_name, durable):
81
+ self._topic_name = topic_name
82
+ self._durable = durable
83
+ self._producer = KafkaProducer({
84
+ 'bootstrap.servers': bootstrap_servers,
85
+ 'acks': 'all' if durable else '1',
86
+ 'message.max.bytes': 10485760,
87
+ })
88
+
89
+ def send(self, message: Any, properties: dict = {}) -> None:
90
+ data_dict = dataclass_to_dict(message)
91
+ json_data = json.dumps(data_dict).encode('utf-8')
92
+
93
+ headers = [
94
+ (k, str(v).encode('utf-8'))
95
+ for k, v in properties.items()
96
+ ] if properties else None
97
+
98
+ self._delivery_error = None
99
+
100
+ def _on_delivery(err, msg):
101
+ if err:
102
+ self._delivery_error = err
103
+
104
+ self._producer.produce(
105
+ topic=self._topic_name,
106
+ value=json_data,
107
+ headers=headers,
108
+ on_delivery=_on_delivery,
109
+ )
110
+ self._producer.flush()
111
+
112
+ if self._delivery_error:
113
+ raise KafkaException(self._delivery_error)
114
+
115
+ def flush(self) -> None:
116
+ self._producer.flush()
117
+
118
+ def close(self) -> None:
119
+ self._producer.flush()
120
+
121
+
122
+ class KafkaBackendConsumer:
123
+ """Consumes from a Kafka topic using a consumer group.
124
+
125
+ Uses confluent-kafka Consumer.poll() for message delivery.
126
+ Not thread-safe — each instance must be used from a single thread,
127
+ which matches the ThreadPoolExecutor pattern in consumer.py.
128
+ """
129
+
130
+ def __init__(self, bootstrap_servers, topic_name, group_id,
131
+ schema_cls, auto_offset_reset='latest'):
132
+ self._bootstrap_servers = bootstrap_servers
133
+ self._topic_name = topic_name
134
+ self._group_id = group_id
135
+ self._schema_cls = schema_cls
136
+ self._auto_offset_reset = auto_offset_reset
137
+ self._consumer = None
138
+
139
+ def _connect(self):
140
+ import time
141
+ t0 = time.monotonic()
142
+
143
+ def _on_assign(consumer, partitions):
144
+ elapsed = time.monotonic() - t0
145
+ logger.info(
146
+ f"Partition assignment for {self._topic_name}: "
147
+ f"{[p.partition for p in partitions]} "
148
+ f"after {elapsed:.1f}s"
149
+ )
150
+
151
+ def _on_revoke(consumer, partitions):
152
+ logger.info(
153
+ f"Partition revoke for {self._topic_name}: "
154
+ f"{[p.partition for p in partitions]}"
155
+ )
156
+
157
+ self._consumer = KafkaConsumer({
158
+ 'bootstrap.servers': self._bootstrap_servers,
159
+ 'group.id': self._group_id,
160
+ 'auto.offset.reset': self._auto_offset_reset,
161
+ 'enable.auto.commit': False,
162
+ 'fetch.message.max.bytes': 10485760,
163
+ # Tighten group coordination timeouts for fast
164
+ # group join on single-member groups.
165
+ 'session.timeout.ms': 6000,
166
+ 'heartbeat.interval.ms': 1000,
167
+ })
168
+ self._consumer.subscribe(
169
+ [self._topic_name],
170
+ on_assign=_on_assign,
171
+ on_revoke=_on_revoke,
172
+ )
173
+ logger.info(
174
+ f"Kafka consumer subscribed: topic={self._topic_name}, "
175
+ f"group={self._group_id}"
176
+ )
177
+
178
+ def _is_alive(self):
179
+ return self._consumer is not None
180
+
181
+ def ensure_connected(self) -> None:
182
+ """Eagerly connect and subscribe.
183
+
184
+ For response/notify consumers this must be called before the
185
+ corresponding request is published, so that the consumer is
186
+ assigned a partition and will see the response message.
187
+ """
188
+ if not self._is_alive():
189
+ self._connect()
190
+
191
+ # Kick off group join. With auto.offset.reset=earliest
192
+ # on response/notify consumers, any messages published
193
+ # before assignment completes will be picked up once
194
+ # the consumer starts polling in receive().
195
+ self._consumer.poll(timeout=0.5)
196
+
197
+ def receive(self, timeout_millis: int = 2000) -> Message:
198
+ """Receive a message. Raises TimeoutError if none available."""
199
+ if not self._is_alive():
200
+ self._connect()
201
+
202
+ timeout_seconds = timeout_millis / 1000.0
203
+ msg = self._consumer.poll(timeout=timeout_seconds)
204
+
205
+ if msg is None:
206
+ raise TimeoutError("No message received within timeout")
207
+
208
+ if msg.error():
209
+ error = msg.error()
210
+ if error.code() == KafkaError._PARTITION_EOF:
211
+ raise TimeoutError("End of partition reached")
212
+ if error.code() == KafkaError.UNKNOWN_TOPIC_OR_PART:
213
+ raise TimeoutError("Topic not yet available")
214
+ raise KafkaException(error)
215
+
216
+ return KafkaMessage(msg, self._schema_cls)
217
+
218
+ def acknowledge(self, message: Message) -> None:
219
+ """Commit the message's offset (next offset to read)."""
220
+ if isinstance(message, KafkaMessage) and message._msg:
221
+ tp = TopicPartition(
222
+ message._msg.topic(),
223
+ message._msg.partition(),
224
+ message._msg.offset() + 1,
225
+ )
226
+ self._consumer.commit(offsets=[tp], asynchronous=False)
227
+
228
+ def negative_acknowledge(self, message: Message) -> None:
229
+ """Seek back to the message's offset for redelivery."""
230
+ if isinstance(message, KafkaMessage) and message._msg:
231
+ tp = TopicPartition(
232
+ message._msg.topic(),
233
+ message._msg.partition(),
234
+ message._msg.offset(),
235
+ )
236
+ self._consumer.seek(tp)
237
+
238
+ def unsubscribe(self) -> None:
239
+ if self._consumer:
240
+ try:
241
+ self._consumer.unsubscribe()
242
+ except Exception:
243
+ pass
244
+
245
+ def close(self) -> None:
246
+ if self._consumer:
247
+ try:
248
+ self._consumer.close()
249
+ except Exception:
250
+ pass
251
+ self._consumer = None
252
+
253
+
254
+ class KafkaBackend:
255
+ """Kafka pub/sub backend using one topic per logical topic."""
256
+
257
+ def __init__(self, bootstrap_servers='localhost:9092',
258
+ security_protocol='PLAINTEXT',
259
+ sasl_mechanism=None,
260
+ sasl_username=None,
261
+ sasl_password=None):
262
+ self._bootstrap_servers = bootstrap_servers
263
+
264
+ # AdminClient config
265
+ self._admin_config = {
266
+ 'bootstrap.servers': bootstrap_servers,
267
+ }
268
+
269
+ if security_protocol != 'PLAINTEXT':
270
+ self._admin_config['security.protocol'] = security_protocol
271
+ if sasl_mechanism:
272
+ self._admin_config['sasl.mechanism'] = sasl_mechanism
273
+ if sasl_username:
274
+ self._admin_config['sasl.username'] = sasl_username
275
+ if sasl_password:
276
+ self._admin_config['sasl.password'] = sasl_password
277
+
278
+ # Topics are created with 1 partition, so only 1 consumer
279
+ # per group can be active. Extra consumers cause rebalance
280
+ # storms that block message delivery.
281
+ self.max_consumer_concurrency = 1
282
+
283
+ logger.info(
284
+ f"Kafka backend: {bootstrap_servers} "
285
+ f"protocol={security_protocol}"
286
+ )
287
+
288
+ def _parse_topic(self, topic_id: str) -> tuple[str, str, bool]:
289
+ """
290
+ Parse topic identifier into Kafka topic name, class, and durability.
291
+
292
+ Format: class:topicspace:topic
293
+ Returns: (topic_name, class, durable)
294
+ """
295
+ if ':' not in topic_id:
296
+ return f'tg.flow.{topic_id}', 'flow', True
297
+
298
+ parts = topic_id.split(':', 2)
299
+ if len(parts) != 3:
300
+ raise ValueError(
301
+ f"Invalid topic format: {topic_id}, "
302
+ f"expected class:topicspace:topic"
303
+ )
304
+
305
+ cls, topicspace, topic = parts
306
+
307
+ if cls == 'flow':
308
+ durable = True
309
+ elif cls in ('request', 'response', 'notify'):
310
+ durable = False
311
+ else:
312
+ raise ValueError(
313
+ f"Invalid topic class: {cls}, "
314
+ f"expected flow, request, response, or notify"
315
+ )
316
+
317
+ # Replace any remaining colons — flow topics can have
318
+ # extra segments (e.g. flow:tg:document-load:default)
319
+ # and Kafka rejects colons in topic names.
320
+ topic_name = f"{topicspace}.{cls}.{topic}".replace(':', '.')
321
+
322
+ return topic_name, cls, durable
323
+
324
+ def _retention_ms(self, cls):
325
+ """Return retention.ms for a topic class."""
326
+ if cls == 'flow':
327
+ return LONG_RETENTION_MS
328
+ return SHORT_RETENTION_MS
329
+
330
+ def create_producer(self, topic: str, schema: type,
331
+ **options) -> BackendProducer:
332
+ topic_name, cls, durable = self._parse_topic(topic)
333
+ logger.debug(f"Creating producer: topic={topic_name}")
334
+ return KafkaBackendProducer(
335
+ self._bootstrap_servers, topic_name, durable,
336
+ )
337
+
338
+ def create_consumer(self, topic: str, subscription: str, schema: type,
339
+ initial_position: str = 'latest',
340
+ **options) -> BackendConsumer:
341
+ """Create a consumer subscribed to a Kafka topic.
342
+
343
+ Behaviour is determined by the topic's class prefix:
344
+ - flow: named consumer group, competing consumers
345
+ - request: named consumer group, competing consumers
346
+ - response: unique consumer group per instance
347
+ - notify: unique consumer group per instance
348
+ """
349
+ topic_name, cls, durable = self._parse_topic(topic)
350
+
351
+ if cls in ('response', 'notify'):
352
+ # Per-subscriber: unique group so every instance sees
353
+ # every message. Filter by correlation ID happens at
354
+ # the Subscriber layer above.
355
+ # Use 'earliest' so that responses published before
356
+ # partition assignment completes are not missed.
357
+ # Each group is unique (UUID) with no committed offsets,
358
+ # so 'earliest' reads from the start of the topic.
359
+ # The correlation ID filter discards non-matching messages.
360
+ group_id = f"{subscription}-{uuid.uuid4()}"
361
+ auto_offset_reset = 'earliest'
362
+ else:
363
+ # Shared: named group, competing consumers
364
+ group_id = subscription
365
+ auto_offset_reset = (
366
+ 'earliest' if initial_position == 'earliest'
367
+ else 'latest'
368
+ )
369
+
370
+ logger.debug(
371
+ f"Creating consumer: topic={topic_name}, "
372
+ f"group={group_id}, cls={cls}"
373
+ )
374
+
375
+ return KafkaBackendConsumer(
376
+ self._bootstrap_servers, topic_name, group_id,
377
+ schema, auto_offset_reset,
378
+ )
379
+
380
+ def _create_topic_sync(self, topic_name, retention_ms):
381
+ """Blocking topic creation via AdminClient."""
382
+ admin = AdminClient(self._admin_config)
383
+ new_topic = NewTopic(
384
+ topic_name,
385
+ num_partitions=1,
386
+ replication_factor=1,
387
+ config={
388
+ 'retention.ms': str(retention_ms),
389
+ },
390
+ )
391
+ fs = admin.create_topics([new_topic])
392
+ for name, f in fs.items():
393
+ try:
394
+ f.result()
395
+ logger.info(f"Created topic: {name}")
396
+ except KafkaException as e:
397
+ # Topic already exists — idempotent
398
+ if e.args[0].code() == KafkaError.TOPIC_ALREADY_EXISTS:
399
+ logger.debug(f"Topic already exists: {name}")
400
+ else:
401
+ raise
402
+
403
+ async def create_topic(self, topic: str) -> None:
404
+ """Create a Kafka topic with appropriate retention."""
405
+ topic_name, cls, durable = self._parse_topic(topic)
406
+ retention_ms = self._retention_ms(cls)
407
+ await asyncio.to_thread(
408
+ self._create_topic_sync, topic_name, retention_ms,
409
+ )
410
+
411
+ def _delete_topic_sync(self, topic_name):
412
+ """Blocking topic deletion via AdminClient."""
413
+ admin = AdminClient(self._admin_config)
414
+ fs = admin.delete_topics([topic_name])
415
+ for name, f in fs.items():
416
+ try:
417
+ f.result()
418
+ logger.info(f"Deleted topic: {name}")
419
+ except KafkaException as e:
420
+ # Topic doesn't exist — idempotent
421
+ if e.args[0].code() == KafkaError.UNKNOWN_TOPIC_OR_PART:
422
+ logger.debug(f"Topic not found: {name}")
423
+ else:
424
+ raise
425
+ except Exception as e:
426
+ logger.debug(f"Topic delete for {name}: {e}")
427
+
428
+ async def delete_topic(self, topic: str) -> None:
429
+ """Delete a Kafka topic."""
430
+ topic_name, cls, durable = self._parse_topic(topic)
431
+ await asyncio.to_thread(self._delete_topic_sync, topic_name)
432
+
433
+ def _topic_exists_sync(self, topic_name):
434
+ """Blocking topic existence check via AdminClient."""
435
+ admin = AdminClient(self._admin_config)
436
+ metadata = admin.list_topics(timeout=10)
437
+ return topic_name in metadata.topics
438
+
439
+ async def topic_exists(self, topic: str) -> bool:
440
+ """Check whether a Kafka topic exists."""
441
+ topic_name, cls, durable = self._parse_topic(topic)
442
+ return await asyncio.to_thread(
443
+ self._topic_exists_sync, topic_name,
444
+ )
445
+
446
+ async def ensure_topic(self, topic: str) -> None:
447
+ """Ensure a topic exists, creating it if necessary."""
448
+ if not await self.topic_exists(topic):
449
+ await self.create_topic(topic)
450
+
451
+ def close(self) -> None:
452
+ pass
@@ -17,6 +17,12 @@ DEFAULT_RABBITMQ_USERNAME = os.getenv("RABBITMQ_USERNAME", 'guest')
17
17
  DEFAULT_RABBITMQ_PASSWORD = os.getenv("RABBITMQ_PASSWORD", 'guest')
18
18
  DEFAULT_RABBITMQ_VHOST = os.getenv("RABBITMQ_VHOST", '/')
19
19
 
20
+ DEFAULT_KAFKA_BOOTSTRAP = os.getenv("KAFKA_BOOTSTRAP_SERVERS", 'kafka:9092')
21
+ DEFAULT_KAFKA_PROTOCOL = os.getenv("KAFKA_SECURITY_PROTOCOL", 'PLAINTEXT')
22
+ DEFAULT_KAFKA_SASL_MECHANISM = os.getenv("KAFKA_SASL_MECHANISM", None)
23
+ DEFAULT_KAFKA_SASL_USERNAME = os.getenv("KAFKA_SASL_USERNAME", None)
24
+ DEFAULT_KAFKA_SASL_PASSWORD = os.getenv("KAFKA_SASL_PASSWORD", None)
25
+
20
26
 
21
27
  def get_pubsub(**config: Any) -> Any:
22
28
  """
@@ -47,6 +53,25 @@ def get_pubsub(**config: Any) -> Any:
47
53
  password=config.get('rabbitmq_password', DEFAULT_RABBITMQ_PASSWORD),
48
54
  vhost=config.get('rabbitmq_vhost', DEFAULT_RABBITMQ_VHOST),
49
55
  )
56
+ elif backend_type == 'kafka':
57
+ from .kafka_backend import KafkaBackend
58
+ return KafkaBackend(
59
+ bootstrap_servers=config.get(
60
+ 'kafka_bootstrap_servers', DEFAULT_KAFKA_BOOTSTRAP,
61
+ ),
62
+ security_protocol=config.get(
63
+ 'kafka_security_protocol', DEFAULT_KAFKA_PROTOCOL,
64
+ ),
65
+ sasl_mechanism=config.get(
66
+ 'kafka_sasl_mechanism', DEFAULT_KAFKA_SASL_MECHANISM,
67
+ ),
68
+ sasl_username=config.get(
69
+ 'kafka_sasl_username', DEFAULT_KAFKA_SASL_USERNAME,
70
+ ),
71
+ sasl_password=config.get(
72
+ 'kafka_sasl_password', DEFAULT_KAFKA_SASL_PASSWORD,
73
+ ),
74
+ )
50
75
  else:
51
76
  raise ValueError(f"Unknown pub/sub backend: {backend_type}")
52
77
 
@@ -65,6 +90,7 @@ def add_pubsub_args(parser: ArgumentParser, standalone: bool = False) -> None:
65
90
  pulsar_host = STANDALONE_PULSAR_HOST if standalone else DEFAULT_PULSAR_HOST
66
91
  pulsar_listener = 'localhost' if standalone else None
67
92
  rabbitmq_host = 'localhost' if standalone else DEFAULT_RABBITMQ_HOST
93
+ kafka_bootstrap = 'localhost:9092' if standalone else DEFAULT_KAFKA_BOOTSTRAP
68
94
 
69
95
  parser.add_argument(
70
96
  '--pubsub-backend',
@@ -122,3 +148,34 @@ def add_pubsub_args(parser: ArgumentParser, standalone: bool = False) -> None:
122
148
  default=DEFAULT_RABBITMQ_VHOST,
123
149
  help=f'RabbitMQ vhost (default: {DEFAULT_RABBITMQ_VHOST})',
124
150
  )
151
+
152
+ # Kafka options
153
+ parser.add_argument(
154
+ '--kafka-bootstrap-servers',
155
+ default=kafka_bootstrap,
156
+ help=f'Kafka bootstrap servers (default: {kafka_bootstrap})',
157
+ )
158
+
159
+ parser.add_argument(
160
+ '--kafka-security-protocol',
161
+ default=DEFAULT_KAFKA_PROTOCOL,
162
+ help=f'Kafka security protocol (default: {DEFAULT_KAFKA_PROTOCOL})',
163
+ )
164
+
165
+ parser.add_argument(
166
+ '--kafka-sasl-mechanism',
167
+ default=DEFAULT_KAFKA_SASL_MECHANISM,
168
+ help='Kafka SASL mechanism',
169
+ )
170
+
171
+ parser.add_argument(
172
+ '--kafka-sasl-username',
173
+ default=DEFAULT_KAFKA_SASL_USERNAME,
174
+ help='Kafka SASL username',
175
+ )
176
+
177
+ parser.add_argument(
178
+ '--kafka-sasl-password',
179
+ default=DEFAULT_KAFKA_SASL_PASSWORD,
180
+ help='Kafka SASL password',
181
+ )
@@ -0,0 +1 @@
1
+ __version__ = "2.3.16"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trustgraph-base
3
- Version: 2.3.14
3
+ Version: 2.3.16
4
4
  Summary: TrustGraph provides a means to run a pipeline of flexible AI processing components in a flexible means to achieve a processing pipeline.
5
5
  Author-email: "trustgraph.ai" <security@trustgraph.ai>
6
6
  Project-URL: Homepage, https://github.com/trustgraph-ai/trustgraph
@@ -13,6 +13,7 @@ Requires-Dist: prometheus-client
13
13
  Requires-Dist: requests
14
14
  Requires-Dist: python-logging-loki
15
15
  Requires-Dist: pika
16
+ Requires-Dist: confluent-kafka
16
17
  Requires-Dist: pyyaml
17
18
 
18
19
  See https://trustgraph.ai/
@@ -44,6 +44,7 @@ trustgraph/base/graph_embeddings_client.py
44
44
  trustgraph/base/graph_embeddings_query_service.py
45
45
  trustgraph/base/graph_embeddings_store_service.py
46
46
  trustgraph/base/graph_rag_client.py
47
+ trustgraph/base/kafka_backend.py
47
48
  trustgraph/base/librarian_client.py
48
49
  trustgraph/base/llm_service.py
49
50
  trustgraph/base/logging.py
@@ -3,4 +3,5 @@ prometheus-client
3
3
  requests
4
4
  python-logging-loki
5
5
  pika
6
+ confluent-kafka
6
7
  pyyaml
@@ -1 +0,0 @@
1
- __version__ = "2.3.14"