diff --git a/Alt-Core/pyproject.toml b/Alt-Core/pyproject.toml index d19a2b2..9121547 100644 --- a/Alt-Core/pyproject.toml +++ b/Alt-Core/pyproject.toml @@ -26,6 +26,8 @@ dependencies = [ "numpy == 2.0.2", "opencv-python == 4.11.0.86", "typing-extensions>=4.0.0", + "types-requests == 2.32.4.20250611", + "mypy == 1.16.1", ] [tool.setuptools] @@ -38,4 +40,4 @@ namespaces = true [project.urls] "Homepage" = "https://github.com/Team488/Alt" "Bug Tracker" = "https://github.com/Team488/Alt/issues" -"repository" = "https://github.com/Team488/Alt" \ No newline at end of file +"repository" = "https://github.com/Team488/Alt" diff --git a/Alt-Core/src/Alt/Core/Agents/Agent.py b/Alt-Core/src/Alt/Core/Agents/Agent.py index dd21c16..5f6245e 100644 --- a/Alt-Core/src/Alt/Core/Agents/Agent.py +++ b/Alt-Core/src/Alt/Core/Agents/Agent.py @@ -1,26 +1,120 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from logging import Logger -from typing import Dict, Optional, Any, ClassVar +from typing import Any, Dict, Optional, Protocol, TypeVar, TYPE_CHECKING from JXTABLES.XTablesClient import XTablesClient from ..Utils.network import DEVICEIP -from ..Operators.LogStreamOperator import LogStreamOperator -from ..Operators.UpdateOperator import UpdateOperator -from ..Operators.PropertyOperator import PropertyOperator -from ..Operators.ConfigOperator import ConfigOperator -from ..Operators.ShareOperator import ShareOperator -from ..Operators.StreamOperator import StreamProxy -from ..Operators.TimeOperator import TimeOperator, Timer -from ..Constants.Teams import TEAM -from ..Constants.AgentConstants import Proxy, ProxyType +if TYPE_CHECKING: + from ..Constants.AgentConstants import Proxy, ProxyType + from ..Constants.Teams import TEAM + from ..Operators.ConfigOperator import ConfigOperator + from ..Operators.LogStreamOperator import LogStreamOperator + from ..Operators.PropertyOperator import PropertyOperator + from ..Operators.ShareOperator import ShareOperator + from ..Operators.StreamOperator import StreamProxy + from ..Operators.TimeOperator import TimeOperator, Timer + from ..Operators.UpdateOperator import UpdateOperator + + +class Agent(Protocol): + hasShutdown: bool = False + hasClosed: bool = False + isCleanedUp: bool = False + isMainThread: bool = False + agentName: str = "" + xclient: XTablesClient + propertyOperator: PropertyOperator + configOperator: ConfigOperator + updateOp: UpdateOperator + timeOp: TimeOperator + Sentinel: Logger + timer: Timer + + def _injectCore( + self, shareOperator: ShareOperator, isMainThread: bool, agentName: str + ) -> None: + ... + + def _injectNEW( + self, + xclient: XTablesClient, # new + propertyOperator: PropertyOperator, # new + configOperator: ConfigOperator, # new + updateOperator: UpdateOperator, # new + timeOperator: TimeOperator, # new + logger: Logger, # static/new + ) -> None: + ... + def getProxy(self, proxyName: str) -> Optional[Proxy]: + ... + + def _setProxies(self, proxies) -> None: + ... + + def _cleanup(self) -> None: + ... + + def getTimer(self) -> Timer: + ... + + def getTeam(self) -> Optional[TEAM]: + ... + + def _runOwnCreate(self): + ... + + @classmethod + def getName(cls) -> str: + ... + + def create(self) -> None: + ... + + def runPeriodic(self) -> None: + ... -class Agent(ABC): + def isRunning(self) -> bool: + ... + + def getDescription(self) -> str: + ... + + def getIntervalMs(self) -> int: + ... + + def forceShutdown(self) -> None: + ... + + def onClose(self) -> None: + ... + + @classmethod + def requestProxies(cls) -> None: + ... + + @classmethod + def addProxyRequest(cls, proxyName: str, proxyType: ProxyType) -> None: + ... + + @classmethod + def _getProxyRequests(cls) -> Dict[str, ProxyType]: + ... + + +TAgent = TypeVar("TAgent", bound=Agent) + + +class AgentBase(ABC): """ Base class for all agents. """ + + _proxyRequests: Dict[str, ProxyType] = {} DEFAULT_LOOP_TIME: int = 0 # 0 ms TIMERS = "timers" @@ -31,8 +125,7 @@ def __init__(self, **kwargs: Any) -> None: self.isCleanedUp: bool = False self.isMainThread: bool = False self.agentName = "" - self.__proxies : Dict[str, Proxy] = {} - + self.__proxies: Dict[str, Proxy] = {} def _injectCore( self, shareOperator: ShareOperator, isMainThread: bool, agentName: str @@ -70,23 +163,24 @@ def _injectNEW( self.timer = self.timeOp.getTimer(self.TIMERS) # other than setting variables, nothing should go here - def _setProxies(self, proxies): + def _setProxies(self, proxies) -> None: self.__proxies = proxies def _updateNetworkProxyInfo(self): - """ Put information about the proxies used on xtables. This can be used for the dashboard, and other things""" + """Put information about the proxies used on xtables. This can be used for the dashboard, and other things""" streamPaths = [] for proxyName, proxy in self.__proxies.items(): if isinstance(proxy, StreamProxy): streamPaths.append(f"{proxyName}|{proxy.getStreamPath()}") - self.propertyOperator.createCustomReadOnlyProperty("streamPaths", streamPaths, addBasePrefix=True, addOperatorPrefix=True) - + self.propertyOperator.createCustomReadOnlyProperty( + "streamPaths", streamPaths, addBasePrefix=True, addOperatorPrefix=True + ) - def getProxy(self, proxyName : str) -> Optional[Proxy]: + def getProxy(self, proxyName: str) -> Optional[Proxy]: return self.__proxies.get(proxyName) - def _cleanup(self): + def _cleanup(self) -> None: # xclient shutdown occasionally failing? # self.xclient.shutdown() self.propertyOperator.deregisterAll() @@ -96,7 +190,7 @@ def getTimer(self) -> Timer: """Use only when needed, and only when associated with agent""" if self.timer is None: raise ValueError("Timer not initialized") - + return self.timer def getTeam(self) -> Optional[TEAM]: @@ -112,18 +206,19 @@ def getTeam(self) -> Optional[TEAM]: return TEAM.BLUE else: return TEAM.RED - + def _runOwnCreate(self): - """ The agent wants to do its own stuff too... okay.""" + """The agent wants to do its own stuff too... okay.""" logIp = f"http://{DEVICEIP}:5000/{self.agentName}/{LogStreamOperator.LOGPATH}" - self.propertyOperator.createCustomReadOnlyProperty("logIP", logIp, addBasePrefix=True, addOperatorPrefix=True) + self.propertyOperator.createCustomReadOnlyProperty( + "logIP", logIp, addBasePrefix=True, addOperatorPrefix=True + ) self.__ensureProxies() self._updateNetworkProxyInfo() - @classmethod def getName(cls): return cls.__name__ @@ -176,28 +271,29 @@ def onClose(self) -> None: pass # ----- proxy methods ----- - def __ensureProxies(self): + def __ensureProxies(self) -> None: for proxyName, proxyType in self._getProxyRequests().items(): - if proxyName not in self.__proxies or type(self.__proxies[proxyName]) is not proxyType: + if ( + proxyName not in self.__proxies + or type(self.__proxies[proxyName]) is not proxyType + ): print(type(self.__proxies[proxyName]) is proxyType) - raise RuntimeError(f"Agent proxies are not correcty initialized!\n{self._getProxyRequests()=}\n{self.__proxies.items()=}") + raise RuntimeError( + f"Agent proxies are not correcty initialized!\n{self._getProxyRequests()=}\n{self.__proxies.items()=}" + ) @classmethod def requestProxies(cls): - """ Override this, and add all of your proxy requests""" + """Override this, and add all of your proxy requests""" pass @classmethod - def addProxyRequest(cls, proxyName : str, proxyType: ProxyType) -> None: - """ Method to request that a stream proxy will be given to this agent to display streams - NOTE: you must override requestProxies() and add your calls to this there, or else it will not be used! + def addProxyRequest(cls, proxyName: str, proxyType: ProxyType) -> None: + """Method to request that a stream proxy will be given to this agent to display streams + NOTE: you must override requestProxies() and add your calls to this there, or else it will not be used! """ - if not hasattr(cls, '_proxyRequests'): - cls._proxyRequests = {} - cls._proxyRequests[proxyName] = proxyType @classmethod def _getProxyRequests(cls) -> Dict[str, ProxyType]: - return getattr(cls, '_proxyRequests', {}) - + return getattr(cls, "_proxyRequests", {}) diff --git a/Alt-Core/src/Alt/Core/Agents/AgentExample.py b/Alt-Core/src/Alt/Core/Agents/AgentExample.py index 7946326..20e9cf6 100644 --- a/Alt-Core/src/Alt/Core/Agents/AgentExample.py +++ b/Alt-Core/src/Alt/Core/Agents/AgentExample.py @@ -1,16 +1,23 @@ -from typing import Any -from .Agent import Agent +from __future__ import annotations +from typing import Any, Optional, TYPE_CHECKING -class AgentExample(Agent): - """ This example agent shows how simple it can be to create a task. - - This agent creates a name property (which allows you to change its name), and then it tells it to you 50 times before ending. +from .Agent import AgentBase + +if TYPE_CHECKING: + from Core.Operators.PropertyOperator import Property + + +class AgentExample(AgentBase): + """This example agent shows how simple it can be to create a task. + + This agent creates a name property (which allows you to change its name), and then it tells it to you 50 times before ending. """ + def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) self.timesRun: int = 0 - self.nameProp = None + self.nameProp: Optional[Property] = None def create(self) -> None: # for example here i can create a propery to configure what to call myself @@ -28,6 +35,7 @@ def runPeriodic(self) -> None: # for example, i can tell the world what im called self.timesRun += 1 + assert self.nameProp is not None name = self.nameProp.get() self.projectNameProp.set(name) self.Sentinel.info(f"My name is {name}") @@ -36,7 +44,9 @@ def onClose(self) -> None: # task cleanup here # for example, i can tell the world that my time has come if self.nameProp is not None: - self.Sentinel.info(f"My time has come. Never forget the name {self.nameProp.get()}!") + self.Sentinel.info( + f"My time has come. Never forget the name {self.nameProp.get()}!" + ) def isRunning(self) -> bool: # condition to keep task running here diff --git a/Alt-Core/src/Alt/Core/Agents/BindableAgent.py b/Alt-Core/src/Alt/Core/Agents/BindableAgent.py index 2f0fa34..ee063c2 100644 --- a/Alt-Core/src/Alt/Core/Agents/BindableAgent.py +++ b/Alt-Core/src/Alt/Core/Agents/BindableAgent.py @@ -1,34 +1,38 @@ +from __future__ import annotations + from abc import abstractmethod from functools import partial + class BindableAgent: - """ When an Agent requires extra keyword arguments, the Neo class cannot just create an instance of it due to it needing arguments. - To fix this, we use the functools partial method. You can imagine it pre-binding the args of the agent, before it gets created. - This way, when the agent needs to be created, the arguments will be stored inside, and Neo can create an instance. + """When an Agent requires extra keyword arguments, the Neo class cannot just create an instance of it due to it needing arguments. + To fix this, we use the functools partial method. You can imagine it pre-binding the args of the agent, before it gets created. + This way, when the agent needs to be created, the arguments will be stored inside, and Neo can create an instance. """ + @classmethod @abstractmethod def bind(cls, *args, **kwargs) -> partial: - """ To make it clearer what arguments an agent needs, please override this bind method and specify the same input arguments as the agents __init__ - At the moment, this is the only way to change the static method signature of bind, so people know what arguments to provide. - In the method body, you can just call cls._getBindedAgent() with the input arguments. + """To make it clearer what arguments an agent needs, please override this bind method and specify the same input arguments as the agents __init__ + At the moment, this is the only way to change the static method signature of bind, so people know what arguments to provide. + In the method body, you can just call cls._getBindedAgent() with the input arguments. - Example: - ``` python - class bindable(Agent, BindableAgent): - # overriding the bind method to make the static method signature clear - @classmethod - def bind(arg1 : str, arg2 : int, ....): - # you can use keyword or positional arguments, but it should match your constructor - return cls._getBindedAgent(arg1, arg2=arg2, ....) + Example: + ``` python + class bindable(Agent, BindableAgent): + # overriding the bind method to make the static method signature clear + @classmethod + def bind(arg1 : str, arg2 : int, ....): + # you can use keyword or positional arguments, but it should match your constructor + return cls._getBindedAgent(arg1, arg2=arg2, ....) - def __init__(arg1 : str, arg2 : int, ....): - # same signature as above, ensures that when neo gets the bound agent, it needs no extra arguments - # init things.... - ``` + def __init__(arg1 : str, arg2 : int, ....): + # same signature as above, ensures that when neo gets the bound agent, it needs no extra arguments + # init things.... + ``` """ pass @classmethod def _getBindedAgent(cls, *args, **kwargs) -> partial: - return partial(cls, *args, **kwargs) \ No newline at end of file + return partial(cls, *args, **kwargs) diff --git a/Alt-Core/src/Alt/Core/Agents/PositionLocalizingAgentBase.py b/Alt-Core/src/Alt/Core/Agents/PositionLocalizingAgentBase.py index 18a6d85..4de0afd 100644 --- a/Alt-Core/src/Alt/Core/Agents/PositionLocalizingAgentBase.py +++ b/Alt-Core/src/Alt/Core/Agents/PositionLocalizingAgentBase.py @@ -1,10 +1,12 @@ +from __future__ import annotations + from typing import Tuple, Any -from .Agent import Agent +from .Agent import AgentBase from ..Utils import NtUtils -class PositionLocalizingAgentBase(Agent): +class PositionLocalizingAgentBase(AgentBase): """Agent -> PositionLocalizingAgentBase Extending Agent with Localizing capabilites. Supports only XTables diff --git a/Alt-Core/src/Alt/Core/Agents/__init__.py b/Alt-Core/src/Alt/Core/Agents/__init__.py index 8ec6b5e..1a05fa3 100644 --- a/Alt-Core/src/Alt/Core/Agents/__init__.py +++ b/Alt-Core/src/Alt/Core/Agents/__init__.py @@ -1,4 +1,13 @@ -from .Agent import Agent +from .Agent import Agent, AgentBase, TAgent from .BindableAgent import BindableAgent from .AgentExample import AgentExample -from .PositionLocalizingAgentBase import PositionLocalizingAgentBase \ No newline at end of file +from .PositionLocalizingAgentBase import PositionLocalizingAgentBase + +__all__ = [ + "Agent", + "AgentBase", + "AgentExample", + "BindableAgent", + "PositionLocalizingAgentBase", + "TAgent", +] diff --git a/Alt-Core/src/Alt/Core/Constants/AgentConstants.py b/Alt-Core/src/Alt/Core/Constants/AgentConstants.py index 4b98edb..73a9c5a 100644 --- a/Alt-Core/src/Alt/Core/Constants/AgentConstants.py +++ b/Alt-Core/src/Alt/Core/Constants/AgentConstants.py @@ -1,43 +1,48 @@ +from __future__ import annotations + from abc import abstractmethod from enum import Enum -from typing import Any, Dict from functools import partial +from typing import Any, Dict, cast + +from Alt.Core.Agents import Agent class ProxyType(Enum): - STREAM = "stream_proxy" + STREAM = "stream_proxy" LOG = "log_proxy" @property def objectName(self): return self.value - + @staticmethod - def getProxyRequests( - agentClass - ) -> Dict[str, "ProxyType"]: + def getProxyRequests(agentClass) -> Dict[str, "ProxyType"]: if isinstance(agentClass, partial): return ProxyType.__getPartialProxyRequests(agentClass) return ProxyType.__getAgentProxyRequests(agentClass) @staticmethod - def __getPartialProxyRequests(agentClass: partial) -> Dict[str, "ProxyType"]: - agentClass.func.requestProxies() - return agentClass.func._getProxyRequests() + def __getPartialProxyRequests( + agentClass: partial[type[Agent]], + ) -> Dict[str, "ProxyType"]: + agentClassType = cast(Agent, agentClass.func) + agentClassType.requestProxies() + return agentClassType._getProxyRequests() @staticmethod - def __getAgentProxyRequests(agentClass) -> Dict[str, "ProxyType"]: + def __getAgentProxyRequests(agentClass: Agent) -> Dict[str, "ProxyType"]: agentClass.requestProxies() return agentClass._getProxyRequests() class Proxy: @abstractmethod - def put(self, value : Any): + def put(self, value: Any): pass @abstractmethod def get(self) -> Any: - pass \ No newline at end of file + pass diff --git a/Alt-Core/src/Alt/Core/Constants/Field.py b/Alt-Core/src/Alt/Core/Constants/Field.py deleted file mode 100644 index f6515aa..0000000 --- a/Alt-Core/src/Alt/Core/Constants/Field.py +++ /dev/null @@ -1,19 +0,0 @@ -from ..Units import Types, Conversions - -class Field: - def __init__(self, width : float, height : float, units : Types.Length = Types.Length.CM): - self.width = width - self.height = height - self.units = units - - def getWidth(self, units: Types.Length = Types.Length.CM) -> float: - result = Conversions.convertLength( - self.width, fromType=self.units, toType=units - ) - return result - - def getHeight(self, units: Types.Length = Types.Length.CM) -> float: - result = Conversions.convertLength( - self.height, fromType=self.units, toType=units - ) - return result diff --git a/Alt-Core/src/Alt/Core/Constants/Teams.py b/Alt-Core/src/Alt/Core/Constants/Teams.py index 7d040bd..dabf765 100644 --- a/Alt-Core/src/Alt/Core/Constants/Teams.py +++ b/Alt-Core/src/Alt/Core/Constants/Teams.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from enum import Enum + class TEAM(Enum): RED = "red" - BLUE = "blue" \ No newline at end of file + BLUE = "blue" diff --git a/Alt-Core/src/Alt/Core/Constants/__init__.py b/Alt-Core/src/Alt/Core/Constants/__init__.py index 4f6967e..7dacfda 100644 --- a/Alt-Core/src/Alt/Core/Constants/__init__.py +++ b/Alt-Core/src/Alt/Core/Constants/__init__.py @@ -1,3 +1,10 @@ -from .Field import Field +from .field import Field from .Teams import TEAM -from .AgentConstants import ProxyType, Proxy \ No newline at end of file +from .AgentConstants import ProxyType, Proxy + +__all__ = [ + "Field", + "Proxy", + "ProxyType", + "TEAM", +] diff --git a/Alt-Core/src/Alt/Core/Constants/field.py b/Alt-Core/src/Alt/Core/Constants/field.py new file mode 100644 index 0000000..5964011 --- /dev/null +++ b/Alt-Core/src/Alt/Core/Constants/field.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Optional, cast + +from ..Units import Conversions, Types + + +class Field: + _instance: Optional[Field] = None + + @classmethod + def getInstance(cls): + if cls._instance is None: + cls._instance = cls(17.55, 8.05, Types.Length.M) + return cls._instance + + def __init__( + self, width: float, height: float, units: Types.Length = Types.Length.CM + ): + self.width = width + self.height = height + self.units = units + + def getWidth(self, units: Types.Length = Types.Length.CM) -> float: + result = Conversions.convertLength( + self.width, fromType=self.units, toType=units + ) + return cast(float, result) + + def getHeight(self, units: Types.Length = Types.Length.CM) -> float: + result = Conversions.convertLength( + self.height, fromType=self.units, toType=units + ) + return cast(float, result) diff --git a/Alt-Core/src/Alt/Core/Neo.py b/Alt-Core/src/Alt/Core/Neo.py index 815d044..a412c52 100644 --- a/Alt-Core/src/Alt/Core/Neo.py +++ b/Alt-Core/src/Alt/Core/Neo.py @@ -10,6 +10,8 @@ management for the entire application. """ +from __future__ import annotations + import os import signal from multiprocessing import Manager @@ -71,11 +73,17 @@ def __init__(self) -> None: Sentinel.info("Starting flask server") self.__flaskOp.start() Sentinel.info("Creating Stream Operator") - self.__streamOp = StreamOperator(app=self.__flaskOp.getApp(), manager=self.__manager) + self.__streamOp = StreamOperator( + app=self.__flaskOp.getApp(), manager=self.__manager + ) Sentinel.info("Creating log stream operator") - self.__logStreamOp = LogStreamOperator(app=self.__flaskOp.getApp(), manager=self.__manager) + self.__logStreamOp = LogStreamOperator( + app=self.__flaskOp.getApp(), manager=self.__manager + ) Sentinel.info("Creating Agent operator") - self.__agentOp = AgentOperator(self.__manager, self.__shareOp, self.__streamOp, self.__logStreamOp) + self.__agentOp = AgentOperator( + self.__manager, self.__shareOp, self.__streamOp, self.__logStreamOp + ) self.__isShutdown = False self.__isDashboardRunning = False # intercept shutdown signals to handle abrupt cleanup @@ -152,9 +160,12 @@ def runDashboard(self) -> None: self.__isDashboardRunning = True try: from Alt.Dashboard import dashboard + dashboard.mainAsync() except ImportError: - Sentinel.fatal("To run the dashboard you must first install the pip package!\nRun:\npip install Alt-Dashboard") + Sentinel.fatal( + "To run the dashboard you must first install the pip package!\nRun:\npip install Alt-Dashboard" + ) else: Sentinel.debug("Dashboard already running or neo shutdown") diff --git a/Alt-Core/src/Alt/Core/Operators/AgentOperator.py b/Alt-Core/src/Alt/Core/Operators/AgentOperator.py index 0c9e733..28cd7ce 100644 --- a/Alt-Core/src/Alt/Core/Operators/AgentOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/AgentOperator.py @@ -12,6 +12,8 @@ (All public and private methods of AgentOperator are documented at the class and function level.) """ +from __future__ import annotations + import logging import multiprocessing import multiprocessing.managers @@ -32,13 +34,14 @@ from .TimeOperator import TimeOperator, Timer from .UpdateOperator import UpdateOperator from .LogStreamOperator import LogStreamOperator -from ..Agents import Agent, BindableAgent +from ..Agents import Agent, BindableAgent, TAgent from .PropertyOperator import LambdaHandler, PropertyOperator, ReadonlyProperty from .LogOperator import getChildLogger from ..Constants.AgentConstants import ProxyType Sentinel = getChildLogger("Agent_Operator") + # subscribes to command request with xtables and then executes when requested class AgentOperator: """ @@ -66,7 +69,7 @@ def __init__( manager: multiprocessing.managers.SyncManager, shareOp: ShareOperator, streamOp: StreamOperator, - logStreamOp : LogStreamOperator + logStreamOp: LogStreamOperator, ) -> None: """ Initializes the AgentOperator. @@ -80,7 +83,7 @@ def __init__( self.__executor: ProcessPoolExecutor = ProcessPoolExecutor() self.__stop: threading.Event = manager.Event() # flag self.futures: list[Future] = [] - self.activeAgentNames : dict[str,int] = {} + self.activeAgentNames: dict[str, int] = {} self.mainAgent: Optional[Agent] = None self.shareOp = shareOp self.streamOp = streamOp @@ -169,10 +172,8 @@ def initalizeProxies( for requestName, proxyType in ProxyType.getProxyRequests(agentClass).items(): # TODO add more if proxyType is ProxyType.STREAM: - proxyDict[ - requestName - ] = self.streamOp.register_stream(agentName) - + proxyDict[requestName] = self.streamOp.register_stream(agentName) + # always create log queue logProxy = self.logStreamOp.register_log_stream(agentName) @@ -280,6 +281,7 @@ def _injectAgent( Returns: Agent: The injected agent. """ + assert agent is not None # injecting stuff shared from core agent._injectCore(shareOperator, isMainThread, agentName) # creating new operators just for this agent and injecting them @@ -292,7 +294,7 @@ def _injectAgent( logProperty = agent.propertyOperator.createCustomReadOnlyProperty( logTable, "None...", addBasePrefix=False, addOperatorPrefix=False ) - lastLogs : list[str] = [] + lastLogs: list[str] = [] logLambda = lambda entry: AgentOperator._handleLog(logProperty, lastLogs, entry) lambda_handler = LambdaHandler(logLambda) @@ -304,9 +306,7 @@ def _injectAgent( @staticmethod def _injectNewOperators( - agent: Agent, - agentName: str, - logProxy: queue.Queue + agent: Agent, agentName: str, logProxy: queue.Queue ) -> None: """ Injects new operator instances and logging handlers into the agent. @@ -326,11 +326,11 @@ def _injectNewOperators( updateOp = UpdateOperator(client, propertyOp) timeOp = TimeOperator(propertyOp) logger = getChildLogger(agentName) - + # add sse handling automatically class SSELogHandler(logging.Handler): closed = False - + def emit(self, record): if not self.closed: try: @@ -340,10 +340,14 @@ def emit(self, record): self.closed = True sse_handler = SSELogHandler() - sse_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + sse_handler.setFormatter( + logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") + ) logger.addHandler(sse_handler) + assert agent is not None + agent._injectNEW( xclient=client, propertyOperator=propertyOp, @@ -355,7 +359,7 @@ def emit(self, record): @staticmethod def _startAgentLoop( - agentClass: type[Agent], + agentClass: type[TAgent], agentName: str, shareOperator: ShareOperator, isMainThread: bool, @@ -379,13 +383,49 @@ def _startAgentLoop( runOnCreate (Optional[Callable[[Agent], None]]): Optional callback to run on agent creation. """ if not isMainThread: - signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGTERM, signal.SIG_IGN) + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) # we use our own interrupts to stop the pool, so ignore signals from sigint """Main agent loop that manages agent lifecycle""" - # helper lambdas + # variables kept through agents life + failed: bool = False + progressStr: str = "starting" + stop = False + + """Initialization part #1 Create agent""" + try: + agent: Agent = agentClass() + except TypeError as e: + # constructor misssing arguments + # three possible mistakes. + # 1) agentClass forgot to implement the bindableAgent interface and call bind before with arguments + # 2) agentClass implemented the bindableAgent interface, it was called, but the bind method didint bind ALL arguments + # 3) agentClass was passed in rather than agentClass.bind(...), which would return a partial + + if isinstance(agentClass, partial): + # bind method must have been called, thus it is case 2) + raise RuntimeError( + f"The bind method of {agentClass} did not cover all arguments! It must cover EVERY argument in __init__() \n{e}" + ) + else: + if issubclass(agentClass, BindableAgent): + # bind method wasnt called, case 3 + raise RuntimeError( + f"""{agentClass} is a bindable agent! You must called {agentClass}'s bind() method,\n + provide arguments, and then pass in the returned object instead! \n{e}""" + ) + else: + # no bind method, but there should be, case 1 + raise RuntimeError( + f"""{agentClass} needs input arguments!. You must implement the bindableAgent interface,\n + override the bind() method with necessary arguments (same as __init__), then call bind() and pass in the returned object instead! \n{e}""" + ) + + assert agent is not None + + # helper lambdas __setStatus: Callable[ [str], bool ] = lambda status: agent.propertyOperator.createCustomReadOnlyProperty( @@ -416,48 +456,16 @@ def __handleException(exception: Exception) -> None: __setErrorLog(tb) Sentinel.error(tb) - - - # variables kept through agents life - failed: bool = False - progressStr: str = "starting" - stop = False - - - """Initialization part #1 Create agent""" try: - agent: Agent = agentClass() - except TypeError as e: - # constructor misssing arguments - # three possible mistakes. - # 1) agentClass forgot to implement the bindableAgent interface and call bind before with arguments - # 2) agentClass implemented the bindableAgent interface, it was called, but the bind method didint bind ALL arguments - # 3) agentClass was passed in rather than agentClass.bind(...), which would return a partial - if isinstance(agentClass, partial): - # bind method must have been called, thus it is case 2) - raise RuntimeError(f"The bind method of {agentClass} did not cover all arguments! It must cover EVERY argument in __init__() \n{e}") - else: - if(issubclass(agentClass, BindableAgent)): - # bind method wasnt called, case 3 - raise RuntimeError(f"""{agentClass} is a bindable agent! You must called {agentClass}'s bind() method,\n - provide arguments, and then pass in the returned object instead! \n{e}""") - else: - # no bind method, but there should be, case 1 - raise RuntimeError(f"""{agentClass} needs input arguments!. You must implement the bindableAgent interface,\n - override the bind() method with necessary arguments (same as __init__), then call bind() and pass in the returned object instead! \n{e}""") - - - try: - - """ Initialization part #3. Inject objects in agent""" + """Initialization part #3. Inject objects in agent""" AgentOperator._injectAgent( agent, agentName, shareOperator, proxies, logProxy, isMainThread ) """ On main thread this is how its set as main agent""" if isMainThread and runOnCreate is not None: runOnCreate(agent) - + except Exception as e: __handleException(e) failed = True @@ -467,7 +475,6 @@ def __handleException(exception: Exception) -> None: __setDescription(agent.getDescription()) __setStatus(progressStr) - # use agents own timer timer: Timer = agent.getTimer() @@ -584,10 +591,8 @@ def waitForAgentsToFinish(self) -> None: self.mainAgent._cleanup() self.mainAgent.propertyOperator.createCustomReadOnlyProperty( - f"{self.mainAgent.agentName}.{AgentOperator.STATUS}", "" - ).set( - "shutdown interrupt" - ) + f"{self.mainAgent.agentName}.{AgentOperator.STATUS}", "" + ).set("shutdown interrupt") Sentinel.info("Main agent finished") @@ -596,4 +601,3 @@ def shutDownNow(self) -> None: Blocks the thread until the executor is finished and all agent processes are shut down. """ self.__executor.shutdown(wait=True, cancel_futures=True) - diff --git a/Alt-Core/src/Alt/Core/Operators/ConfigOperator.py b/Alt-Core/src/Alt/Core/Operators/ConfigOperator.py index 99a650e..bf8b6e6 100644 --- a/Alt-Core/src/Alt/Core/Operators/ConfigOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/ConfigOperator.py @@ -13,6 +13,8 @@ (All public and private methods are documented at the class and function level.) """ +from __future__ import annotations + import os import json import codecs @@ -26,10 +28,12 @@ Sentinel = getChildLogger("Config_Operator") + class ConfigType(Enum): """ Enum representing supported configuration file types and their load/save handlers. """ + NUMPY = "numpy" JSON = "json" @@ -120,6 +124,7 @@ def save(self, path: str, content: Any) -> None: else: raise ValueError(f"Unsupported config type: {self}") + class ConfigOperator: """ Handles loading, saving, and accessing configuration files in various formats. @@ -248,11 +253,9 @@ def getAllFileNames(self) -> List[str]: List[str]: List of config file names. """ return list(self.configMap.keys()) - + @staticmethod - def staticLoad( - fileName: str - ) -> Optional[Tuple[Any, float]]: + def staticLoad(fileName: str) -> Optional[Tuple[Any, float]]: """ Load a file from one of the configured save paths and return its content and modification time. @@ -281,11 +284,9 @@ def staticLoad( Sentinel.debug(f"{path} does not exist!") return None - + @staticmethod - def staticWrite( - filename: str, content: Any, config_type: ConfigType - ) -> bool: + def staticWrite(filename: str, content: Any, config_type: ConfigType) -> bool: """ Try to save content to all configured save paths using the given config type. @@ -309,5 +310,3 @@ def staticWrite( Sentinel.debug(e) Sentinel.info(f"Failed to write to {file_path}") return success - - diff --git a/Alt-Core/src/Alt/Core/Operators/FlaskOperator.py b/Alt-Core/src/Alt/Core/Operators/FlaskOperator.py index d02eda4..b5c6389 100644 --- a/Alt-Core/src/Alt/Core/Operators/FlaskOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/FlaskOperator.py @@ -7,15 +7,14 @@ FlaskOperator: Manages the lifecycle of a Flask server in a separate thread. """ +from __future__ import annotations + import threading from flask import Flask from werkzeug.serving import make_server from .LogOperator import getChildLogger - - - Sentinel = getChildLogger("Stream_Operator") @@ -31,6 +30,7 @@ class FlaskOperator: server_thread (threading.Thread): The thread running the server. running (bool): Indicates if the server is running. """ + PORT = 5000 HOST = "0.0.0.0" @@ -77,4 +77,3 @@ def shutdown(self): self.server_thread.join() Sentinel.info("Flask Server stopped.") - diff --git a/Alt-Core/src/Alt/Core/Operators/LogOperator.py b/Alt-Core/src/Alt/Core/Operators/LogOperator.py index 7430183..4f4cc68 100644 --- a/Alt-Core/src/Alt/Core/Operators/LogOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/LogOperator.py @@ -11,6 +11,8 @@ getChildLogger: Gets a child logger derived from the central logger. """ +from __future__ import annotations + import os import socket import logging @@ -43,9 +45,7 @@ def createLogger(loggerName: str): logger.setLevel(LOGLEVEL) if not logger.handlers: - log_filename = os.path.join( - BASELOGDIR, f"{fullName}_{getTimeStr()}.log" - ) + log_filename = os.path.join(BASELOGDIR, f"{fullName}_{getTimeStr()}.log") file_handler = logging.FileHandler(log_filename, mode="a", encoding="utf-8") file_handler.setFormatter( logging.Formatter( @@ -56,6 +56,7 @@ def createLogger(loggerName: str): return logger + def setMainLogger(mainLogger: logging.Logger): """ Set the main logger instance. @@ -66,6 +67,7 @@ def setMainLogger(mainLogger: logging.Logger): global Sentinel Sentinel = mainLogger + def createAndSetMain(loggerName: str): """ Create and set the main logger using the given name. @@ -75,6 +77,7 @@ def createAndSetMain(loggerName: str): """ setMainLogger(createLogger(loggerName)) + Sentinel = createLogger("Core") diff --git a/Alt-Core/src/Alt/Core/Operators/LogStreamOperator.py b/Alt-Core/src/Alt/Core/Operators/LogStreamOperator.py index 1ff0872..62f1bb8 100644 --- a/Alt-Core/src/Alt/Core/Operators/LogStreamOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/LogStreamOperator.py @@ -7,6 +7,8 @@ LogStreamOperator: Registers and manages log streams for SSE endpoints. """ +from __future__ import annotations + import queue from flask import Flask, Response, stream_with_context import functools @@ -28,7 +30,9 @@ class LogStreamOperator: log_streams (dict): Dictionary to store log queues. running (bool): Indicates if the log stream operator is running. """ + LOGPATH = "logs" + def __init__(self, app: Flask, manager: multiprocessing.managers.SyncManager): """ Initializes the LogStreamOperator. @@ -39,10 +43,12 @@ def __init__(self, app: Flask, manager: multiprocessing.managers.SyncManager): """ self.app = app self.manager = manager - self.log_streams : dict[str, dict[str, queue.Queue]] = {} # Dictionary to store log queues + self.log_streams: dict[ + str, dict[str, queue.Queue] + ] = {} # Dictionary to store log queues self.running = True - def register_log_stream(self, name : str) -> queue.Queue: + def register_log_stream(self, name: str) -> queue.Queue: """ Registers a new SSE log stream and returns a Queue for pushing logs to it. @@ -89,6 +95,7 @@ def _create_log_view_func(self, generator_func, name): Returns: function: The Flask view function for the log stream. """ + @functools.wraps(generator_func) def view_func(): response = Response( diff --git a/Alt-Core/src/Alt/Core/Operators/ShareOperator.py b/Alt-Core/src/Alt/Core/Operators/ShareOperator.py index 2e23b30..4173b6e 100644 --- a/Alt-Core/src/Alt/Core/Operators/ShareOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/ShareOperator.py @@ -6,6 +6,8 @@ ShareOperator: Uses a multiprocessing dict to share memory across agents. """ +from __future__ import annotations + from typing import Any, Optional from .LogOperator import getChildLogger diff --git a/Alt-Core/src/Alt/Core/Operators/StreamOperator.py b/Alt-Core/src/Alt/Core/Operators/StreamOperator.py index 0c7a8d1..d111be9 100644 --- a/Alt-Core/src/Alt/Core/Operators/StreamOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/StreamOperator.py @@ -1,11 +1,13 @@ """ This module provides a streaming service using Flask and multiprocessing. It allows the registration and management of multiple video streams, -handling frame capture and serving them as MJPEG over HTTP. +handling frame capture and serving them as MJPEG over HTTP. The StreamOperator class manages the streams, while the StreamProxy class serves as a wrapper for accessing and manipulating the stream data between processes. """ +from __future__ import annotations + import functools import multiprocessing import time @@ -21,6 +23,7 @@ class serves as a wrapper for accessing and manipulating the stream data between Sentinel = getChildLogger("Stream_Operator") + class StreamOperator: """Handles the management of multiple MJPEG video streams. @@ -37,9 +40,9 @@ class StreamOperator: manager (multiprocessing.managers.SyncManager): Manager for multiprocessing. running (bool): Flag to indicate if the server is running. """ - + STREAMPATH = "stream.mjpg" - + def __init__(self, app: Flask, manager: multiprocessing.managers.SyncManager): """Initializes a StreamOperator instance. @@ -72,7 +75,7 @@ def register_stream(self, name: str) -> "StreamProxy": streamProxy = StreamProxy(self.manager.dict(), streamPath) self.streams[name] = streamProxy - + def generate_frames(streamProxy: StreamProxy): """Generator function to yield MJPEG frames from the stream proxy. @@ -97,10 +100,12 @@ def generate_frames(streamProxy: StreamProxy): b"--frame\r\n" b"Content-Type: image/jpeg\r\n\r\n" + jpeg.tobytes() + b"\r\n\r\n" ) - + self.app.add_url_rule( f"/{name}/{self.STREAMPATH}", - view_func=self._create_view_func(lambda : generate_frames(streamProxy), name), + view_func=self._create_view_func( + lambda: generate_frames(streamProxy), name + ), ) Sentinel.info(f"Registered new stream: {name} at '{name}/{self.STREAMPATH}'") return streamProxy @@ -115,12 +120,14 @@ def _create_view_func(self, generate_frames_func, name: str): Returns: function: A Flask view function for serving the video stream. """ + @functools.wraps(generate_frames_func) def view_func(): return Response( stream_with_context(generate_frames_func()), # Stream with context mimetype="multipart/x-mixed-replace; boundary=frame", ) + view_func.__name__ = f"stream_{name}_view" return view_func @@ -145,6 +152,7 @@ def close_stream(self, name: str): del self.streams[name] Sentinel.info(f"Closed stream: {name}") + class StreamProxy(Proxy): """Wrapper for accessing and manipulating a stream from another process. @@ -155,7 +163,7 @@ class StreamProxy(Proxy): __streamDict (managers.DictProxy): The underlying dictionary proxy holding stream data. """ - + def __init__(self, streamDict: managers.DictProxy, streamPath: str): """Initializes a StreamProxy instance. @@ -216,4 +224,4 @@ def __setstate__(self, state): Args: state (dict): The state to restore from. """ - self.__streamDict = state \ No newline at end of file + self.__streamDict = state diff --git a/Alt-Core/src/Alt/Core/Operators/TimeOperator.py b/Alt-Core/src/Alt/Core/Operators/TimeOperator.py index 1294fb7..6986d1a 100644 --- a/Alt-Core/src/Alt/Core/Operators/TimeOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/TimeOperator.py @@ -7,6 +7,8 @@ Timer: Measures and records timing information for code blocks. """ +from __future__ import annotations + import time from typing import Dict, Generator from .LogOperator import getChildLogger @@ -14,6 +16,7 @@ Sentinel = getChildLogger("Time Operator") + class TimeOperator: """ Manages timing operations for performance monitoring. @@ -47,7 +50,7 @@ def getTimer(self, timeName: str = TIMENAME) -> "Timer": """ if timeName in self.timerMap: return self.timerMap[timeName] - + timer = self.__createTimer(timeName) self.timerMap[timeName] = timer return timer @@ -70,8 +73,10 @@ def __createTimer(self, timeName: str) -> "Timer": raise ValueError(f"Could not create property child for timer {timeName}") return Timer(timeName, timeTable) + from contextlib import contextmanager + class Timer: """ Measures and records performance timing information. @@ -159,4 +164,4 @@ def run(self, subTimerName: str = "main") -> Generator[None, None, None]: try: yield finally: - self.measureAndUpdate(subTimerName) \ No newline at end of file + self.measureAndUpdate(subTimerName) diff --git a/Alt-Core/src/Alt/Core/Operators/UpdateOperator.py b/Alt-Core/src/Alt/Core/Operators/UpdateOperator.py index ad465c6..05187da 100644 --- a/Alt-Core/src/Alt/Core/Operators/UpdateOperator.py +++ b/Alt-Core/src/Alt/Core/Operators/UpdateOperator.py @@ -1,14 +1,16 @@ """ UpdateOperator Module. -This module defines the `UpdateOperator` class, responsible for managing global updates -across multiple running agents. It facilitates adding, subscribing, and deregistering +This module defines the `UpdateOperator` class, responsible for managing global updates +across multiple running agents. It facilitates adding, subscribing, and deregistering updates while ensuring that conflicts are minimized through locking mechanisms. Classes: UpdateOperator: Manages global updates, subscriptions, and locking for agent coordination. """ +from __future__ import annotations + import random import time from collections import defaultdict @@ -19,13 +21,14 @@ Sentinel = getChildLogger("Update_Operator") + class UpdateOperator: """ Manages global updates for multiple agents. - The `UpdateOperator` class provides functionalities to create, set, - and get global updates among agents. It handles subscriptions to updates - and ensures there are no conflicts in the updates being processed + The `UpdateOperator` class provides functionalities to create, set, + and get global updates among agents. It handles subscriptions to updates + and ensures there are no conflicts in the updates being processed by employing locking mechanisms. Attributes: @@ -44,7 +47,9 @@ class UpdateOperator: EMPTYLOCK: int = -1 BUSYLOCK: int = 1 - def __init__(self, xclient: XTablesClient, propertyOperator: PropertyOperator) -> None: + def __init__( + self, xclient: XTablesClient, propertyOperator: PropertyOperator + ) -> None: """Initializes the UpdateOperator with the given XTablesClient and PropertyOperator. Args: @@ -70,13 +75,13 @@ def withLock(self, runnable: Callable[[], None], isAllRunning: bool) -> None: # Add uniform random delay to avoid collision in reading empty lock. delay = random.uniform(0, 0.1) # Max 100ms time.sleep(delay) - + if self.__xclient.getDouble(PATHLOCK) is not None: while self.__xclient.getDouble(PATHLOCK) != self.EMPTYLOCK: time.sleep(0.01) # Wait for lock to open self.__xclient.putDouble(PATHLOCK, self.BUSYLOCK) - + try: runnable() except Exception as e: @@ -90,6 +95,7 @@ def addToRunning(self, uniqueUpdateName: str) -> None: Args: uniqueUpdateName (str): The unique update name to be added to the running agents list. """ + def add(isAllRunning: bool): RUNNINGPATH = ( self.ALLRUNNINGAGENTPATHS @@ -102,13 +108,15 @@ def add(isAllRunning: bool): if uniqueUpdateName not in existingNames: existingNames.append(uniqueUpdateName) self.__xclient.putStringList(RUNNINGPATH, existingNames) - + addAll = lambda: add(True) addCur = lambda: add(False) self.withLock(addAll, isAllRunning=True) # Always add to all running self.withLock(addCur, isAllRunning=False) # Also always add to current running - def getCurrentlyRunning(self, pathFilter: Optional[Callable[[str], bool]] = None) -> List[str]: + def getCurrentlyRunning( + self, pathFilter: Optional[Callable[[str], bool]] = None + ) -> List[str]: """Gets a filtered list of currently running agent paths. Args: @@ -141,7 +149,9 @@ def addGlobalUpdate(self, updateName: str, value: Any) -> None: addOperatorPrefix=True, ).set(value) - def createGlobalUpdate(self, updateName: str, default: Any = None, loadIfSaved: bool = True) -> Property: + def createGlobalUpdate( + self, updateName: str, default: Any = None, loadIfSaved: bool = True + ) -> Property: """Creates a global update with a specified name, default value, and load options. Args: @@ -227,7 +237,7 @@ def subscribeAllGlobalUpdates( newSubscribers: List[str] = [] runningPaths = self.getCurrentlyRunning(pathFilter) fullTables: Set[str] = set() - + for runningPath in runningPaths: fullTable = f"{runningPath}.{updateName}" fullTables.add(fullTable) @@ -276,6 +286,7 @@ def unsubscribeToAllGlobalUpdates( def deregister(self) -> None: """Deregisters the agent and cleans up all subscriptions.""" + def remove(): existingNames = self.__xclient.getStringList( self.CURRENTLYRUNNINGAGENTPATHS @@ -287,8 +298,10 @@ def remove(): existingNames.remove(self.uniqueUpdateName) self.__xclient.putStringList(self.CURRENTLYRUNNINGAGENTPATHS, existingNames) - self.withLock(remove, isAllRunning=False) # Only currently running removes paths - + self.withLock( + remove, isAllRunning=False + ) # Only currently running removes paths + for updateName, fullTables in self.__subscribedUpdates.items(): runOnClose = self.__subscribedRunOnClose.get(updateName) subscriber = self.__subscribedSubscriber.get(updateName) @@ -297,4 +310,4 @@ def remove(): for fullTable in fullTables: self.__xclient.unsubscribe(fullTable, subscriber) if runOnClose is not None: - runOnClose(fullTable) \ No newline at end of file + runOnClose(fullTable) diff --git a/Alt-Core/src/Alt/Core/TestUtils/AgentTests.py b/Alt-Core/src/Alt/Core/TestUtils/AgentTests.py index 08ddf99..d4b75b9 100644 --- a/Alt-Core/src/Alt/Core/TestUtils/AgentTests.py +++ b/Alt-Core/src/Alt/Core/TestUtils/AgentTests.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import time from .. import Neo from ..Agents import Agent @@ -6,14 +8,17 @@ Sentinel = getChildLogger("Agent_Tester") -def test_agent(agentClass : type[Agent]): + +def test_agent(agentClass: type[Agent]): _test_interrupting_running_agent(agentClass) _test_getting_agent_info(agentClass) _test_running_agent_for_some_time(agentClass) -def _test_interrupting_running_agent(agentClass : type[Agent], numAgents = 4): - print("---------------Starting _test_interrupting_running_agent_main()---------------") +def _test_interrupting_running_agent(agentClass: type[Agent], numAgents=4): + print( + "---------------Starting _test_interrupting_running_agent_main()---------------" + ) ensureXTablesServer() n = Neo() @@ -24,23 +29,23 @@ def _test_interrupting_running_agent(agentClass : type[Agent], numAgents = 4): n.shutDown() -def _test_getting_agent_info(agentClass : type[Agent]): + +def _test_getting_agent_info(agentClass: type[Agent]): print("---------------Starting _test_getting_agent_info()---------------") - + agent = agentClass() agent.getName() agent.getDescription() agent.getIntervalMs() -def _test_running_agent_for_some_time(agentClass : type[Agent], n_seconds = 5): + +def _test_running_agent_for_some_time(agentClass: type[Agent], n_seconds=5): print("---------------Starting test_running_agent_for_some_time()---------------") - + ensureXTablesServer() n = Neo() - + n.wakeAgent(agentClass, isMainThread=False) time.sleep(n_seconds) n.shutDown() - - diff --git a/Alt-Core/src/Alt/Core/TestUtils/ensureXTablesServer.py b/Alt-Core/src/Alt/Core/TestUtils/ensureXTablesServer.py index c167fae..5140991 100644 --- a/Alt-Core/src/Alt/Core/TestUtils/ensureXTablesServer.py +++ b/Alt-Core/src/Alt/Core/TestUtils/ensureXTablesServer.py @@ -1,9 +1,14 @@ +from __future__ import annotations + import requests import subprocess import socket from ..Utils.files import __get_user_data_dir, download_file -__LATESTXTABLEPATH = "https://github.com/Kobeeeef/XTABLES/releases/download/v5.0.0/XTABLES.jar" +__LATESTXTABLEPATH = ( + "https://github.com/Kobeeeef/XTABLES/releases/download/v5.0.0/XTABLES.jar" +) + # Function to check if MDNS hostname is resolved def __check_mdns_exists(hostname): @@ -15,6 +20,7 @@ def __check_mdns_exists(hostname): print(f"{hostname} not found") return False + # Function to check if port 4880 is open def __check_port_open(host="localhost", port=4880): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -27,6 +33,7 @@ def __check_port_open(host="localhost", port=4880): except (socket.timeout, socket.error): return False + # Ensure XTables server is running def ensureXTablesServer(): # First, try checking via MDNS @@ -56,7 +63,11 @@ def ensureXTablesServer(): # Attempt to start the server using Java try: # Use Popen to run the process asynchronously - process = subprocess.Popen(["java", "-jar", str(xtables_path)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process = subprocess.Popen( + ["java", "-jar", str(xtables_path)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) print(f"Started XTables server in the background with PID {process.pid}") # Optionally, you can read the process output asynchronously if needed @@ -70,4 +81,3 @@ def ensureXTablesServer(): print(f"Java not found or XTables jar missing! {e}") except subprocess.CalledProcessError as e: print(f"Java ran but XTables failed to start properly! {e}") - diff --git a/Alt-Core/src/Alt/Core/Units/Conversions.py b/Alt-Core/src/Alt/Core/Units/Conversions.py index 694f98a..cb46ee5 100644 --- a/Alt-Core/src/Alt/Core/Units/Conversions.py +++ b/Alt-Core/src/Alt/Core/Units/Conversions.py @@ -1,11 +1,13 @@ +from __future__ import annotations + from collections.abc import Iterable -from typing import Any, Union, TypeVar, Callable, Tuple +from typing import Any, Union, TypeVar, Callable, Tuple, cast from . import Types, Measurements from ..Constants import Field # Type variables for better typing NumericType = Union[float, int] -T = TypeVar('T') +T = TypeVar("T") ConversionFunction = Callable[[NumericType], T] @@ -50,43 +52,44 @@ def cmtoy(cm: NumericType) -> float: def toint( - value: Union[Iterable[NumericType], NumericType] + value: Union[Iterable[NumericType], NumericType], ) -> Union[Tuple[int, ...], int, None]: """ Convert numeric value(s) to integer - + Args: value: A single numeric value or an iterable of numeric values - + Returns: The input value(s) converted to integer, or None if conversion fails """ return __convert(value, int) -def invertY(y: NumericType, lengthType : Types.Length = Types.Length.CM) -> float: +def invertY(y: NumericType, lengthType: Types.Length = Types.Length.CM) -> float: """ Invert the Y coordinate relative to field height - + Args: yCM: Y coordinate in units specified, or by default CM - + Returns: Inverted Y coordinate (field height - Y) """ - return Field.fieldHeight.getLength(lengthType) - y + return Field.getInstance().getHeight(lengthType) - y -def invertX(x: NumericType, lengthType : Types.Length = Types.Length.CM) -> float: + +def invertX(x: NumericType, lengthType: Types.Length = Types.Length.CM) -> float: """ Invert the X coordinate relative to field width - + Args: xCM: X coordinate in the units specified, or by default CM - + Returns: Inverted X coordinate (field width - X) """ - return Field.fieldWidth.getLength(lengthType) - x + return Field.getInstance().getWidth(lengthType) - x def convertLength( @@ -96,20 +99,22 @@ def convertLength( ) -> Union[Tuple[float, ...], float, None]: """ Convert length value(s) between different units - + Args: value: Length value(s) to convert fromType: Source unit type toType: Target unit type - + Returns: Converted value(s), or None if conversion fails """ # "default case" if fromType == toType: - return value + return cast(Union[Tuple[float, ...], float, None], value) - convertLengthFunc = lambda value: Measurements.Length.convert(value, fromType, toType) + convertLengthFunc = lambda value: Measurements.Length.convert( + value, fromType, toType + ) return __convert(value, convertLengthFunc) @@ -120,30 +125,31 @@ def convertRotation( ) -> Union[Tuple[float, ...], float, None]: """ Convert rotation value(s) between different units - + Args: value: Rotation value(s) to convert fromType: Source unit type (degrees or radians) toType: Target unit type (degrees or radians) - + Returns: Converted value(s), or None if conversion fails """ - convertRotFunc = lambda value: Measurements.Rotation.convert(value, fromType, toType) + convertRotFunc = lambda value: Measurements.Rotation.convert( + value, fromType, toType + ) return __convert(value, convertRotFunc) def __convert( - value: Union[Iterable[Any], Any], - convertFunction: Callable[[Any], T] + value: Union[Iterable[Any], Any], convertFunction: Callable[[Any], T] ) -> Union[Tuple[T, ...], T, None]: """ Internal helper function to apply a conversion function to a value or iterable of values - + Args: value: Value(s) to convert convertFunction: Function to apply to each value - + Returns: Converted value(s), or None if conversion fails """ diff --git a/Alt-Core/src/Alt/Core/Units/Measurements.py b/Alt-Core/src/Alt/Core/Units/Measurements.py index b1544f9..6f21aac 100644 --- a/Alt-Core/src/Alt/Core/Units/Measurements.py +++ b/Alt-Core/src/Alt/Core/Units/Measurements.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import math from dataclasses import dataclass from Alt.Core.Units import Types, Conversions + @dataclass class Length: """ @@ -105,6 +108,8 @@ def fromLengthType(cls, length: float, lengthType: Types.Length) -> "Length": if lengthType == Types.Length.FT: return cls.fromFeet(length) + assert False, f"Unexpected lengthType: '{lengthType}'" + def getCm(self) -> float: """ Returns the length in centimeters. @@ -156,6 +161,8 @@ def getAsLengthType(self, lengthType: Types.Length) -> float: if lengthType == Types.Length.FT: return self.getFeet() + assert False, f"Unexpected lengthType: '{lengthType}'" + @classmethod def convert(cls, value: float, fromL: Types.Length, toL: Types.Length) -> float: return cls.fromLengthType(value, fromL).getAsLengthType(toL) @@ -222,12 +229,16 @@ def fromRadians(cls, radians: float): return obj @classmethod - def fromRotationType(cls, length: float, rotationType: Types.Rotation) -> "Rotation": + def fromRotationType( + cls, length: float, rotationType: Types.Rotation + ) -> "Rotation": if rotationType == Types.Rotation.Rad: return cls.fromRadians(length) if rotationType == Types.Rotation.Deg: return cls.fromDegrees(length) + assert False, f"Unexpected rotationType: '{rotationType}'" + def getDegrees(self) -> float: """ Returns the rotation in degrees. @@ -255,6 +266,8 @@ def getAsRotationType(self, rotationType: Types.Rotation) -> float: if rotationType == Types.Rotation.Deg: return self.getDegrees() + assert False, f"Unexpected rotationType: '{rotationType}'" + @classmethod def convert(cls, value: float, fromR: Types.Rotation, toR: Types.Rotation) -> float: return cls.fromRotationType(value, fromR).getAsRotationType(toR) diff --git a/Alt-Core/src/Alt/Core/Units/Poses.py b/Alt-Core/src/Alt/Core/Units/Poses.py index 4381165..8c00fc1 100644 --- a/Alt-Core/src/Alt/Core/Units/Poses.py +++ b/Alt-Core/src/Alt/Core/Units/Poses.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from .Types import Length, Rotation + class Transform3d: """ Coordinate system: @@ -13,21 +16,23 @@ class Transform3d: y: left z: out of screen (up) """ - def __init__(self, x : float, y : float, z : float, units : Length = Length.CM): + + def __init__(self, x: float, y: float, z: float, units: Length = Length.CM): self.x = x self.y = y self.z = z self.units = units - def add(self, other : "Transform3d") -> "Transform3d": + def add(self, other: "Transform3d") -> "Transform3d": return Transform3d(self.x + other.x, self.y + other.y, self.z + other.z) - def subtract(self, other : "Transform3d") -> "Transform3d": + def subtract(self, other: "Transform3d") -> "Transform3d": return self.add(other.negate()) def negate(self) -> "Transform3d": return Transform3d(-self.x, -self.y, -self.z) + class Pose3d: """ Coordinate system: @@ -41,11 +46,13 @@ class Pose3d: y: left z: out of screen (up) """ - def __init__(self, transform : Transform3d, units : Length = Length.CM): + + def __init__(self, transform: Transform3d, units: Length = Length.CM): self.transform = transform self.units = units # TODO rotation + class Pose2d: """ Coordinate system: @@ -53,15 +60,23 @@ class Pose2d: ^ x | | - y <----o + y <----o x: forwards y: left yaw: rotation counter clockwise from x -> y """ - def __init__(self, x : float, y : float, yaw : float, lengthUnit : Length = Length.CM, rotationUnit : Rotation = Rotation.Rad): + + def __init__( + self, + x: float, + y: float, + yaw: float, + lengthUnit: Length = Length.CM, + rotationUnit: Rotation = Rotation.Rad, + ): self.x = x self.y = y self.yaw = yaw self.lengthUnit = lengthUnit - self.rotrotationUnit = rotationUnit \ No newline at end of file + self.rotrotationUnit = rotationUnit diff --git a/Alt-Core/src/Alt/Core/Units/Types.py b/Alt-Core/src/Alt/Core/Units/Types.py index de52b57..97d2b98 100644 --- a/Alt-Core/src/Alt/Core/Units/Types.py +++ b/Alt-Core/src/Alt/Core/Units/Types.py @@ -1,5 +1,8 @@ +from __future__ import annotations + from enum import Enum + class Length(Enum): """ Enum for representing length types. diff --git a/Alt-Core/src/Alt/Core/Utils/NtUtils.py b/Alt-Core/src/Alt/Core/Utils/NtUtils.py index 7d143d7..ebab9b0 100644 --- a/Alt-Core/src/Alt/Core/Utils/NtUtils.py +++ b/Alt-Core/src/Alt/Core/Utils/NtUtils.py @@ -7,9 +7,12 @@ - Functions to unpack 2D and 3D pose data from byte sequences. """ +from __future__ import annotations + import struct from typing import Tuple + def getPose2dFromBytes(bytes: bytes) -> Tuple[float, float, float]: """ Unpacks a 2D pose from a bytes object. @@ -20,9 +23,12 @@ def getPose2dFromBytes(bytes: bytes) -> Tuple[float, float, float]: Returns: Tuple[float, float, float]: A tuple (x, y, rotation) where units depend on what was packed. """ - return struct.unpack('ddd', bytes) + return struct.unpack("ddd", bytes) + -def getTranslation3dFromBytes(bytes: bytes) -> Tuple[Tuple[float, float, float], Tuple[float, float, float, float]]: +def getTranslation3dFromBytes( + bytes: bytes, +) -> Tuple[Tuple[float, float, float], Tuple[float, float, float, float]]: """ Unpacks a 3D pose (translation and rotation) from a bytes object. @@ -35,7 +41,7 @@ def getTranslation3dFromBytes(bytes: bytes) -> Tuple[Tuple[float, float, float], - translation as (x, y, z) - rotation as quaternion (w, x, y, z) """ - ret = struct.unpack('ddddddd', bytes) + ret = struct.unpack("ddddddd", bytes) translation = ret[:3] rotation = ret[3:] return (translation, rotation) diff --git a/Alt-Core/src/Alt/Core/Utils/XTutils.py b/Alt-Core/src/Alt/Core/Utils/XTutils.py index 8ee9afe..6e527a2 100644 --- a/Alt-Core/src/Alt/Core/Utils/XTutils.py +++ b/Alt-Core/src/Alt/Core/Utils/XTutils.py @@ -6,6 +6,8 @@ - A function to convert a sequence of points into a list of XTableValues_pb2.Coordinate objects. """ +from __future__ import annotations + from typing import List, Sequence, Union, Tuple from JXTABLES import XTableValues_pb2 diff --git a/Alt-Core/src/Alt/Core/Utils/files.py b/Alt-Core/src/Alt/Core/Utils/files.py index e50e11a..056e690 100644 --- a/Alt-Core/src/Alt/Core/Utils/files.py +++ b/Alt-Core/src/Alt/Core/Utils/files.py @@ -8,6 +8,8 @@ - A simple file downloader using HTTP(S). """ +from __future__ import annotations + import os import platform import requests @@ -16,6 +18,7 @@ APPNAME = "Alt" + def __get_user_data_dir() -> Path: """ Determines and returns the path to the application's user data directory, @@ -32,7 +35,11 @@ def __get_user_data_dir() -> Path: system = platform.system() if system == "Windows": - base_dir = Path(os.getenv('LOCALAPPDATA') or os.getenv('APPDATA')) + local_app_data = os.getenv("LOCALAPPDATA") + app_data = os.getenv("APPDATA") + assert local_app_data is not None + assert app_data is not None + base_dir = Path(local_app_data or app_data) elif system == "Darwin": # MacOS base_dir = Path.home() / "Library" / "Application Support" else: # Linux and others @@ -42,7 +49,8 @@ def __get_user_data_dir() -> Path: app_dir.mkdir(parents=True, exist_ok=True) return app_dir -user_data_dir = __get_user_data_dir() + +user_data_dir = __get_user_data_dir() def __get_user_tmp_dir() -> Path: @@ -60,7 +68,7 @@ def __get_user_tmp_dir() -> Path: system = platform.system() if system == "Windows": - base_tmp = Path(os.getenv('TEMP') or tempfile.gettempdir()) + base_tmp = Path(os.getenv("TEMP") or tempfile.gettempdir()) elif system == "Darwin": # macOS base_tmp = Path("/tmp") else: # Linux and others @@ -70,7 +78,8 @@ def __get_user_tmp_dir() -> Path: app_tmp_dir.mkdir(parents=True, exist_ok=True) return app_tmp_dir -user_tmp_dir = __get_user_tmp_dir() + +user_tmp_dir = __get_user_tmp_dir() def download_file(url, target_path: Path) -> None: @@ -87,4 +96,4 @@ def download_file(url, target_path: Path) -> None: response = requests.get(url) response.raise_for_status() target_path.write_bytes(response.content) - print(f"Downloaded to {target_path}") \ No newline at end of file + print(f"Downloaded to {target_path}") diff --git a/Alt-Core/src/Alt/Core/Utils/network.py b/Alt-Core/src/Alt/Core/Utils/network.py index e3d944f..3897419 100644 --- a/Alt-Core/src/Alt/Core/Utils/network.py +++ b/Alt-Core/src/Alt/Core/Utils/network.py @@ -9,6 +9,8 @@ - A function to programmatically determine the local IP address. """ +from __future__ import annotations + import socket DEVICEHOSTNAME = socket.gethostname() @@ -38,7 +40,3 @@ def get_local_ip(): DEVICEIP = get_local_ip() - - - - diff --git a/Alt-Core/src/Alt/Core/Utils/timeFmt.py b/Alt-Core/src/Alt/Core/Utils/timeFmt.py index 477b6e0..d7e11a0 100644 --- a/Alt-Core/src/Alt/Core/Utils/timeFmt.py +++ b/Alt-Core/src/Alt/Core/Utils/timeFmt.py @@ -6,10 +6,12 @@ - A function to get the current time or a given time as a formatted string. """ +from __future__ import annotations + from time import localtime, strftime -def getTimeStr(time = None): +def getTimeStr(time=None): """ Returns a formatted time string. diff --git a/Alt-Core/tests/Constants/test_agentConstants.py b/Alt-Core/tests/Constants/test_agentConstants.py index 4fd98b7..90cef60 100644 --- a/Alt-Core/tests/Constants/test_agentConstants.py +++ b/Alt-Core/tests/Constants/test_agentConstants.py @@ -1,9 +1,12 @@ +from __future__ import annotations + + def test_get_proxy_requests(): from functools import partial - from Alt.Core.Agents import Agent + from Alt.Core.Agents import AgentBase from Alt.Core.Constants.AgentConstants import ProxyType - class TestAgent(Agent): + class TestAgent(AgentBase): @classmethod def requestProxies(cls): super().addProxyRequest("test1", ProxyType.STREAM) @@ -13,5 +16,13 @@ def requestProxies(cls): partialTestAgent = partial(TestAgent) - assert ProxyType.getProxyRequests(TestAgent) == {"test1": ProxyType.LOG, "test2": ProxyType.LOG, "test3" : ProxyType.LOG} - assert ProxyType.getProxyRequests(partialTestAgent) == {"test1": ProxyType.LOG, "test2": ProxyType.LOG, "test3" : ProxyType.LOG} + assert ProxyType.getProxyRequests(TestAgent) == { + "test1": ProxyType.LOG, + "test2": ProxyType.LOG, + "test3": ProxyType.LOG, + } + assert ProxyType.getProxyRequests(partialTestAgent) == { + "test1": ProxyType.LOG, + "test2": ProxyType.LOG, + "test3": ProxyType.LOG, + }