Initial commit
Change-Id: I03c605dfaa884b6c7aab3e52259855eb2110fac5
This commit is contained in:
parent
0e4e1a6ada
commit
19bdfeea69
191
.gitignore
vendored
191
.gitignore
vendored
@ -1,2 +1,193 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
.nox/
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
#Local Ignore
|
||||||
|
/example/exn
|
||||||
|
111
Readme.md
Normal file
111
Readme.md
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
Of course, let's integrate everything into a cohesive guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **Using the `exn` Module: A Comprehensive Guide**
|
||||||
|
|
||||||
|
The `exn` module serves as a connector, enabling communication between various components in a messaging infrastructure. This guide will take you through the basics to advanced usage of the module.
|
||||||
|
|
||||||
|
### **Overview**
|
||||||
|
|
||||||
|
- **Core Components:**
|
||||||
|
- **Bootstrap**: A foundational class setting up the readiness state.
|
||||||
|
- **CoreHandler**: Manages connection start, message reception, and timed tasks.
|
||||||
|
- **EXN**: Main connector class initializing connections and configurations.
|
||||||
|
|
||||||
|
### **Basic Usage**
|
||||||
|
|
||||||
|
1. **Initialize** the `EXN` class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector = connector.EXN('ui', bootstrap=Bootstrap())
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Run** the connector to start:
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector.start()
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Advanced Usage**
|
||||||
|
|
||||||
|
#### **1. Enabling Health and State Monitoring**
|
||||||
|
|
||||||
|
The `EXN` class offers two flags: `enable_health` and `enable_state`.
|
||||||
|
|
||||||
|
- **`enable_health`:** Enables a scheduled publisher that sends a health-check ping at regular intervals.
|
||||||
|
- **`enable_state`:** Activates the `StatePublisher` to manage and signal the lifecycle states of the component.
|
||||||
|
|
||||||
|
**Gradual Implementation:**
|
||||||
|
|
||||||
|
**a. Basic Setup (No Flags):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector = connector.EXN('ui', bootstrap=Bootstrap())
|
||||||
|
```
|
||||||
|
|
||||||
|
**b. Health Monitoring:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector = connector.EXN('ui', bootstrap=Bootstrap(), enable_health=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
**c. Lifecycle State Monitoring:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
connector = connector.EXN('ui', bootstrap=Bootstrap(), enable_health=True, enable_state=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2. The `ready` Function**
|
||||||
|
|
||||||
|
The `ready` function in your Bootstrap class is called when the component is initialized. Use it to perform specific operations at startup:
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def ready(self, context):
|
||||||
|
if context.has_publisher('state'):
|
||||||
|
context.publishers['state'].starting()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3. The Importance of the 'key' Argument**
|
||||||
|
|
||||||
|
Each publisher is identified using a unique 'key', which is crucial when handling multiple publishers:
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
context.publishers['state'].starting()
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, 'state' is the 'key' for a specific publisher, directing it to send a 'starting' signal.
|
||||||
|
|
||||||
|
### **Working with Context**
|
||||||
|
|
||||||
|
The `Context` class in the `core` section aids in managing publishers and consumers within the application. Here's how:
|
||||||
|
|
||||||
|
- Register a publisher:
|
||||||
|
|
||||||
|
```python
|
||||||
|
context.register_publisher(publisher)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Check if a publisher exists:
|
||||||
|
|
||||||
|
```python
|
||||||
|
context.has_publisher('key')
|
||||||
|
```
|
||||||
|
|
||||||
|
- Build an address from a link:
|
||||||
|
|
||||||
|
```python
|
||||||
|
context.build_address_from_link(link)
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Conclusion**
|
||||||
|
|
||||||
|
The `exn` module provides a robust platform for component communication, health and lifecycle management, and more. Whether you're looking for simple connectivity or advanced message routing with multiple publishers, this module has got you covered.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This documentation offers an inclusive overview and guide for the `exn` module. Tailor it as per your specific project requirements or as the module evolves.
|
1
example/requirements.txt
Normal file
1
example/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
python-dotenv
|
51
example/test_exn_publisher.py
Normal file
51
example/test_exn_publisher.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from exn import connector, core
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logging.getLogger('exn.connector').setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class MyHandler(connector.ConnectorHandler):
|
||||||
|
|
||||||
|
def ready(self, context):
|
||||||
|
if context.has_publisher('state'):
|
||||||
|
context.publishers['state'].starting()
|
||||||
|
context.publishers['state'].started()
|
||||||
|
context.publishers['state'].custom('forecasting')
|
||||||
|
context.publishers['state'].stopping()
|
||||||
|
context.publishers['state'].stopped()
|
||||||
|
|
||||||
|
context.publishers['config'].send({
|
||||||
|
'hello': 'world'
|
||||||
|
})
|
||||||
|
context.publishers['preferences'].send()
|
||||||
|
|
||||||
|
|
||||||
|
class MyPublisher(core.publisher.Publisher):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__('preferences', 'preferences.changed', True)
|
||||||
|
|
||||||
|
def send(self, body={}):
|
||||||
|
body.update({
|
||||||
|
"preferences": {
|
||||||
|
"dark_mode": True
|
||||||
|
}
|
||||||
|
})
|
||||||
|
super(MyPublisher, self).send(body)
|
||||||
|
|
||||||
|
|
||||||
|
connector = connector.EXN('ui', handler=MyHandler()
|
||||||
|
, publishers=[
|
||||||
|
core.publisher.Publisher('config', 'config', True),
|
||||||
|
MyPublisher(),
|
||||||
|
],
|
||||||
|
enable_health=True, enable_state=False
|
||||||
|
,url='localhost'
|
||||||
|
,port=5672
|
||||||
|
,username="admin"
|
||||||
|
,password="adming"
|
||||||
|
)
|
||||||
|
|
||||||
|
connector.start()
|
27
example/test_exn_receiver.py
Normal file
27
example/test_exn_receiver.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from exn import connector, core
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logging.getLogger('exn.connector').setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class Bootstrap(connector.ConnectorHandler):
|
||||||
|
|
||||||
|
def on_message(self, key, address, body, context, **kwargs):
|
||||||
|
logging.info(f"Received {key} => {address}")
|
||||||
|
if key == 'ui_health':
|
||||||
|
logging.info(f"I am healthy => {body}")
|
||||||
|
|
||||||
|
if key == 'ui_all':
|
||||||
|
logging.info(f"These are my preferences => {body}")
|
||||||
|
|
||||||
|
|
||||||
|
connector = connector.EXN('ui', handler=Bootstrap(),
|
||||||
|
consumers=[
|
||||||
|
core.consumer.Consumer('ui_health', 'health', topic=True),
|
||||||
|
core.consumer.Consumer('ui_all', 'eu.nebulouscloud.ui.preferences.>', topic=True,
|
||||||
|
fqdn=True)
|
||||||
|
])
|
||||||
|
|
||||||
|
connector.start()
|
1
exn/__init__.py
Normal file
1
exn/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import connector
|
155
exn/connector.py
Normal file
155
exn/connector.py
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from proton.handlers import MessagingHandler
|
||||||
|
from proton.reactor import Container
|
||||||
|
|
||||||
|
from .core import context as core_context, state_publisher, schedule_publisher
|
||||||
|
from .settings import base
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class ConnectorHandler:
|
||||||
|
def __init__(self):
|
||||||
|
self.initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
def set_ready(self,ready, ctx:core_context.Context):
|
||||||
|
self.initialized = ready
|
||||||
|
self.ready(ctx)
|
||||||
|
|
||||||
|
def ready(self, ctx:core_context.Context):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_message(self, key, address, body, context, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CoreHandler(MessagingHandler):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
context,
|
||||||
|
handler: ConnectorHandler,
|
||||||
|
publishers = [],
|
||||||
|
consumers = [],
|
||||||
|
):
|
||||||
|
super(CoreHandler, self).__init__()
|
||||||
|
self.context=context
|
||||||
|
self.publishers=publishers
|
||||||
|
self.consumers=consumers
|
||||||
|
self.handler = handler
|
||||||
|
self.conn = None
|
||||||
|
|
||||||
|
def on_start(self, event) -> None:
|
||||||
|
|
||||||
|
self.conn = event.container.connect(self.context.connection)
|
||||||
|
for publisher in self.publishers:
|
||||||
|
_logger.info(f"{publisher.address} registering sender")
|
||||||
|
address = self.context.build_address_from_link(publisher)
|
||||||
|
publisher.set(event.container.create_sender(self.conn,address))
|
||||||
|
self.context.register_publisher(publisher)
|
||||||
|
_logger.debug(f"{self.context.base} Registering timer { hasattr(publisher, 'delay')}")
|
||||||
|
if hasattr(publisher, "delay"):
|
||||||
|
_logger.debug(f"{self.context.base} Registering timer")
|
||||||
|
event.reactor.schedule(publisher.delay, self)
|
||||||
|
|
||||||
|
for consumer in self.consumers:
|
||||||
|
address = self.context.build_address_from_link(consumer)
|
||||||
|
_logger.info(f"{self.context.base} Registering consumer {address}")
|
||||||
|
consumer.set(event.container.create_receiver(self.conn, address))
|
||||||
|
self.context.register_consumers(consumer)
|
||||||
|
|
||||||
|
def on_sendable(self, event):
|
||||||
|
if not self.handler.initialized:
|
||||||
|
self.handler.set_ready(True, self.context)
|
||||||
|
|
||||||
|
def on_timer_task(self, event):
|
||||||
|
_logger.debug(f"{self.context.base} On timer")
|
||||||
|
for publisher in self._delay_publishers():
|
||||||
|
publisher.send()
|
||||||
|
event.reactor.schedule(publisher.delay, self)
|
||||||
|
|
||||||
|
def on_message(self, event):
|
||||||
|
try:
|
||||||
|
for consumer in self.consumers:
|
||||||
|
if consumer.should_handle(event):
|
||||||
|
_logger.debug(f"Received message: {event.message.address}")
|
||||||
|
self.handler.on_message(consumer.key, event.message.address, event.message.body, self.context, event=event)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Received message: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.conn:
|
||||||
|
self.conn.close()
|
||||||
|
else:
|
||||||
|
_logger.warning(f"{self.context.base} No open connection")
|
||||||
|
|
||||||
|
def _delay_publishers(self):
|
||||||
|
return [p for p in self.publishers if hasattr(p,'delay')]
|
||||||
|
|
||||||
|
|
||||||
|
class EXN:
|
||||||
|
def __init__(self, component=None,
|
||||||
|
handler:ConnectorHandler = None,
|
||||||
|
publishers=[],
|
||||||
|
consumers=[],
|
||||||
|
**kwargs):
|
||||||
|
|
||||||
|
# Load .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Validate and set connector
|
||||||
|
if not component:
|
||||||
|
_logger.error("Component cannot be empty or None")
|
||||||
|
raise ValueError("Component cannot be empty or None")
|
||||||
|
self.component = component
|
||||||
|
self.handler = handler
|
||||||
|
|
||||||
|
self.url = kwargs.get('url',os.getenv('NEBULOUS_BROKER_URL'))
|
||||||
|
self.port = kwargs.get('port', os.getenv('NEBULOUS_BROKER_PORT'))
|
||||||
|
self.username = kwargs.get('username',os.getenv('NEBULOUS_BROKER_USERNAME'))
|
||||||
|
self.password = kwargs.get('password', os.getenv('NEBULOUS_BROKER_PASSWORD'))
|
||||||
|
|
||||||
|
# Validate attributes
|
||||||
|
if not self.url:
|
||||||
|
_logger.error("URL cannot be empty or None")
|
||||||
|
raise ValueError("URL cannot be empty or None")
|
||||||
|
if not self.port:
|
||||||
|
_logger.error("PORT cannot be empty or None")
|
||||||
|
raise ValueError("PORT cannot be empty or None")
|
||||||
|
if not self.username:
|
||||||
|
_logger.error("USERNAME cannot be empty or None")
|
||||||
|
raise ValueError("USERNAME cannot be empty or None")
|
||||||
|
if not self.password:
|
||||||
|
_logger.error("PASSWORD cannot be empty or None")
|
||||||
|
raise ValueError("PASSWORD cannot be empty or None")
|
||||||
|
|
||||||
|
ctx = core_context.Context(
|
||||||
|
connection=f"{self.url}:{self.port}",
|
||||||
|
base=f"{base.NEBULOUS_BASE_NAME}.{self.component}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if kwargs.get("enable_state",False):
|
||||||
|
publishers.append(state_publisher.Publisher())
|
||||||
|
|
||||||
|
if kwargs.get("enable_health",False):
|
||||||
|
publishers.append(schedule_publisher.Publisher(
|
||||||
|
base.NEBULOUS_DEFAULT_HEALTH_CHECK_TIMEOUT,
|
||||||
|
'health',
|
||||||
|
'health',
|
||||||
|
True))
|
||||||
|
|
||||||
|
core_handler = CoreHandler(
|
||||||
|
ctx,
|
||||||
|
handler,
|
||||||
|
publishers,
|
||||||
|
consumers
|
||||||
|
)
|
||||||
|
|
||||||
|
self.container = Container(core_handler)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.container.run()
|
8
exn/core/__init__.py
Normal file
8
exn/core/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
from . import context
|
||||||
|
from . import publisher
|
||||||
|
from . import consumer
|
||||||
|
from . import state_publisher
|
||||||
|
from . import schedule_publisher
|
||||||
|
|
17
exn/core/consumer.py
Normal file
17
exn/core/consumer.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
from proton import Message, Event
|
||||||
|
from . import link
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Consumer(link.Link):
|
||||||
|
|
||||||
|
def on_message(self, body, **kwargs):
|
||||||
|
_logger.debug(f"{self.address} Got {body} ")
|
||||||
|
|
||||||
|
def should_handle(self, event: Event):
|
||||||
|
if event.link == self._link:
|
||||||
|
return True
|
63
exn/core/context.py
Normal file
63
exn/core/context.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from . import link
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
|
|
||||||
|
def __init__(self, connection, base):
|
||||||
|
|
||||||
|
self.connection = connection
|
||||||
|
self.base = base
|
||||||
|
self.publishers = {}
|
||||||
|
self.consumers = {}
|
||||||
|
|
||||||
|
def get_publisher(self, key):
|
||||||
|
if key in self.publishers:
|
||||||
|
return self.publishers[key]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def has_publisher(self, key):
|
||||||
|
return key in self.publishers
|
||||||
|
|
||||||
|
def has_consumer(self, key):
|
||||||
|
return key in self.consumers
|
||||||
|
|
||||||
|
def register_publisher(self, publisher):
|
||||||
|
self.publishers[publisher.key] = publisher
|
||||||
|
|
||||||
|
def register_consumers(self, consumer):
|
||||||
|
self.consumers[consumer.key] = consumer
|
||||||
|
|
||||||
|
def build_address_from_link(self, link: link.Link):
|
||||||
|
|
||||||
|
if link.fqdn:
|
||||||
|
address = link.address
|
||||||
|
if link.topic and not link.address.startswith("topic://"):
|
||||||
|
address = f"topic://{address}"
|
||||||
|
return address
|
||||||
|
|
||||||
|
address = f"{self.base}.{link.address}"
|
||||||
|
if link.topic:
|
||||||
|
address = f"topic://{address}"
|
||||||
|
|
||||||
|
return address
|
||||||
|
|
||||||
|
def match_address(self, l: link.Link, event):
|
||||||
|
|
||||||
|
if not event \
|
||||||
|
or not event.message \
|
||||||
|
or not event.message.address:
|
||||||
|
return False
|
||||||
|
|
||||||
|
address = self.build_address_from_link(l)
|
||||||
|
return address == event.message.address
|
||||||
|
|
||||||
|
def build_address(self, *actions, topic=False):
|
||||||
|
|
||||||
|
if len(actions) <= 0:
|
||||||
|
return self.base
|
||||||
|
|
||||||
|
address = f"{self.base}.{'.'.join(actions)}"
|
||||||
|
if topic:
|
||||||
|
address = f"topic://{address}"
|
||||||
|
|
||||||
|
return address
|
18
exn/core/link.py
Normal file
18
exn/core/link.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
from proton import Link as pLink
|
||||||
|
class Link:
|
||||||
|
|
||||||
|
fqdn=False
|
||||||
|
def __init__(self, key, address, topic=False, fqdn=False):
|
||||||
|
|
||||||
|
self.key = key
|
||||||
|
self.address = address
|
||||||
|
self._link = None
|
||||||
|
self.topic= topic
|
||||||
|
self.fqdn= fqdn
|
||||||
|
|
||||||
|
|
||||||
|
def set(self, link:pLink):
|
||||||
|
# The proton container creates a sender
|
||||||
|
# so we just use composition instead of extension
|
||||||
|
self._link = link
|
33
exn/core/publisher.py
Normal file
33
exn/core/publisher.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from proton import Message
|
||||||
|
|
||||||
|
from . import link
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Publisher(link.Link):
|
||||||
|
|
||||||
|
def send(self, body=None):
|
||||||
|
if not body:
|
||||||
|
body = {}
|
||||||
|
|
||||||
|
_logger.debug(f"{self.address} Sending {body} ")
|
||||||
|
msg = self._prepare_message(body)
|
||||||
|
self._link.send(msg)
|
||||||
|
|
||||||
|
def _prepare_message(self, body=None):
|
||||||
|
|
||||||
|
if not body:
|
||||||
|
body = {}
|
||||||
|
|
||||||
|
send = {"when": datetime.datetime.utcnow().isoformat()}
|
||||||
|
send.update(body)
|
||||||
|
msg = Message(
|
||||||
|
address=self._link.target.address,
|
||||||
|
body=send
|
||||||
|
)
|
||||||
|
msg.content_type = "application/json"
|
||||||
|
return msg
|
14
exn/core/schedule_publisher.py
Normal file
14
exn/core/schedule_publisher.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from . import publisher
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Publisher(publisher.Publisher):
|
||||||
|
send_next = False
|
||||||
|
delay = 15
|
||||||
|
|
||||||
|
def __init__(self, delay, key, address, topic=False):
|
||||||
|
super(Publisher, self).__init__(key, address, topic)
|
||||||
|
self.delay = delay
|
45
exn/core/state_publisher.py
Normal file
45
exn/core/state_publisher.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from proton import Message
|
||||||
|
|
||||||
|
from . import publisher
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class States(Enum):
|
||||||
|
|
||||||
|
STARTING = "starting"
|
||||||
|
STARTED = "started"
|
||||||
|
READY = "ready"
|
||||||
|
STOPPING = "stopping"
|
||||||
|
STOPPED = "stopped"
|
||||||
|
|
||||||
|
class Publisher(publisher.Publisher):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("state","state", True)
|
||||||
|
|
||||||
|
def _send_message(self, message_type):
|
||||||
|
self.send({"state": message_type,"message": None})
|
||||||
|
|
||||||
|
def starting(self):
|
||||||
|
self._send_message(States.STARTING)
|
||||||
|
|
||||||
|
def started(self):
|
||||||
|
self._send_message(States.STARTED)
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
self._send_message(States.READY)
|
||||||
|
|
||||||
|
def stopping(self):
|
||||||
|
self._send_message(States.STOPPING)
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
self._send_message(States.STOPPED)
|
||||||
|
|
||||||
|
def custom(self, state):
|
||||||
|
self._send_message(state)
|
1
exn/settings/__init__.py
Normal file
1
exn/settings/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import base
|
2
exn/settings/base.py
Normal file
2
exn/settings/base.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
NEBULOUS_BASE_NAME="eu.nebulouscloud"
|
||||||
|
NEBULOUS_DEFAULT_HEALTH_CHECK_TIMEOUT=15
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
python-qpid-proton
|
Loading…
Reference in New Issue
Block a user