Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
312 changes: 237 additions & 75 deletions README.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ dependencies = [
"pydantic >= 2.0",
"pydantic-settings >= 2.0",
"requests >= 2.30",
"python-socketio >= 5.11",
"websocket-client >= 1.8"
]

[project.urls]
Expand Down
7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
annotated-types==0.6.0
bidict==0.23.1
build==1.2.1
certifi==2024.2.2
charset-normalizer==3.3.2
h11==0.14.0
idna==3.7
importlib_metadata==7.1.0
packaging==24.0
Expand All @@ -10,8 +12,13 @@ pydantic-settings==2.2.1
pydantic_core==2.18.2
pyproject_hooks==1.1.0
python-dotenv==1.0.1
python-engineio==4.9.0
python-socketio==5.11.2
requests==2.31.0
simple-websocket==1.0.0
tomli==2.0.1
typing_extensions==4.11.0
urllib3==2.2.1
websocket-client==1.8.0
wsproto==1.2.0
zipp==3.18.1
4 changes: 3 additions & 1 deletion src/alfred/base/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

class ConfigurationDict(TypedDict):
base_url: Text
realtime_url: Text
version: int


Expand All @@ -15,7 +16,7 @@ class Configuration:
@staticmethod
def default() -> ConfigurationDict:
"""
Returns default client configuration. Currently targets Alfred V1.
Returns default client configuration. Currently, targets Alfred V1.
"""
return Configuration.v1()

Expand All @@ -28,4 +29,5 @@ def v1(overrides: Optional[OverridesDict] = None) -> ConfigurationDict:
return {
"version": 1,
"base_url": overrides.get("base_url", "https://app.tagshelf.com"),
"realtime_url": overrides.get("realtime_url", "https://sockets.tagshelf.io"),
}
10 changes: 10 additions & 0 deletions src/alfred/base/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from enum import Enum

from src.alfred.http.typed import ResponseType

# Response type/header mapping
Expand All @@ -6,3 +8,11 @@
ResponseType.TEXT: "text/plain",
ResponseType.XML: "application/xml",
}


class EventName(Enum):
"""
Enumeration of event names.
"""
JOB_EVENT = "job_event"
FILE_EVENT = "file_event"
7 changes: 7 additions & 0 deletions src/alfred/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ConnectionError(Exception):
"""
Raised when a connection error occurs.
"""
def __init__(self, message="A connection error occurred"):
self.message = message
super().__init__(self.message)
120 changes: 120 additions & 0 deletions src/alfred/realtime/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 3rd Party Imports
import socketio

import src.alfred.exceptions
from src.alfred.base.config import ConfigurationDict
from src.alfred.base.constants import EventName
from src.alfred.http.typed import AuthConfiguration
from src.alfred.utils import logging, setup_logger


class AlfredRealTimeClient:
def __init__(self, config: ConfigurationDict, auth_config: AuthConfiguration, verbose=False):
"""
Initializes the AlfredRealTimeClient class.

Args:
config (ConfigurationDict): The configuration dictionary.
auth_config (AuthConfiguration): The authentication configuration.
verbose (bool, optional): Whether to print verbose output. Defaults to False.
"""
self.socket = socketio.Client()
self.verbose = verbose
self.config = config
self.auth_config = auth_config
self.base_url = config.get("realtime_url")

# Initialize logger
self.logger = logging.getLogger("alfred-python")
if self.logger.level == logging.NOTSET:
setup_logger({
"level": "DEBUG" if verbose else "INFO",
"name": "alfred-python"
})
print(self.logger.level)

# Subscribe to connection life-cycle events.
self.socket.on('connect', self.__on_connect)
self.socket.on('disconnect', self.__on_disconnect)
self.socket.on('connect_error', self.__on_connect_error)

# Establish connection with verbose output if enabled
if self.verbose:
self.logger.debug("Attempting to establish a connection...")
try:
self.socket.connect(f"{self.base_url}?apiKey={auth_config.get('api_key')}")
except Exception as err:
raise src.alfred.exceptions.ConnectionError(f"Could not establish connection with server: {err}")

def __on_connect(self):
"""
Handles the 'connect' event.
"""
self.logger.info(f"Successfully connected to: {self.base_url}")

def __on_disconnect(self):
"""
Handles the 'disconnect' event.
"""
self.logger.info("Disconnected from the server.")

def __on_connect_error(self, err):
"""
Handles the 'connect_error' event.

Args:
err (str): The error message.
"""
self.logger.info("Connection error: %s", err)
self.disconnect()
raise Exception(f"Failed to connect to {self.base_url}: {err}")

def __callback(self, event: str, callback):
"""
Wrapper function to subscribe a specific event.

Args:
event (str): The event name.
callback (function): The callback function to handle the event.
"""
def handle_event(data):
if self.verbose:
self.logger.debug(f"Event {event} received: %s", data)
callback(data)

self.socket.on(event, handle_event)

def on_file_event(self, callback):
"""
Handles the 'file_event' event.

Args:
callback (function): The callback function to handle the event.
"""
self.__callback(EventName.FILE_EVENT.value, callback)

def on_job_event(self, callback):
"""
Handles the 'job_event' event.

Args:
callback (function): The callback function to handle the event.
"""
self.__callback(EventName.JOB_EVENT.value, callback)

def on(self, event: str, callback):
"""
Handles a specific event.

Args:
event (str): The event name.
callback (function): The callback function to handle the event.
"""
self.__callback(event, callback)

def disconnect(self):
"""
Disconnects client from the server.
"""
self.logger.info("Closing connection...")
self.socket.disconnect()
4 changes: 2 additions & 2 deletions src/alfred/typings/misc.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Native imports
from typing import TypedDict
from typing import TypedDict, Union


# Typed dictionaries
class LoggingOptions(TypedDict):
level: int
level: Union[str, int]
name: str
format: str
papertrail_host: str
Expand Down