Преглед на файлове

Add comprehensive typing to the public interface + mypy tox check

Strongly type CloudBridge's public API so downstream users get a
well-typed interface, even though the internal providers remain untyped.
Because the interface layer expresses the entire return-type web
(create_provider() -> CloudProvider -> ComputeService -> InstanceService
-> ResultList[Instance] -> Instance), annotating it plus shipping a PEP
561 py.typed marker makes the whole API typed for consumers regardless of
provider internals.

- Annotate the full interface layer (interfaces/{resources,services,
  subservices,provider,exceptions}.py), factory.py and __init__.py.
- Make PageableObjectMixin and ResultList generic so list()/iteration are
  typed (ResultList[Instance], etc.); add `from __future__ import
  annotations` and TYPE_CHECKING blocks to break import cycles.
- Ship cloudbridge/py.typed via [tool.setuptools.package-data].
- Add a strict [tool.mypy] baseline over the whole package, with base.*
  and providers.* temporarily exempted (ignore_errors) as a gradual
  ratchet -- remove a module from that list as it gets typed.
- Add a lean `mypy` tox env (skip_install) to envlist and run it in the
  CI lint job; add mypy>=1.11 to the dev extra.

Interface behaviour is unchanged: @abstractproperty / __metaclass__ are
kept as-is (no modernization), so runtime abstractness of the untyped
providers is unaffected.
Nuwan Goonasekera преди 1 ден
родител
ревизия
bcd6d79171

+ 3 - 0
.github/workflows/integration.yaml

@@ -60,6 +60,9 @@ jobs:
       - name: Run tox
       - name: Run tox
         run: tox -e lint
         run: tox -e lint
 
 
+      - name: Run mypy
+        run: tox -e mypy
+
   mock:
   mock:
     name: Mock-provider tests
     name: Mock-provider tests
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest

+ 8 - 5
cloudbridge/__init__.py

@@ -1,11 +1,12 @@
 """Library setup."""
 """Library setup."""
 import logging
 import logging
+from typing import Any
 
 
 # Current version of the library
 # Current version of the library
 __version__ = '4.1.0'
 __version__ = '4.1.0'
 
 
 
 
-def get_version():
+def get_version() -> str:
     """
     """
     Return a string with the current version of the library.
     Return a string with the current version of the library.
 
 
@@ -15,7 +16,7 @@ def get_version():
     return __version__
     return __version__
 
 
 
 
-def init_logging():
+def init_logging() -> None:
     """
     """
     Initialize logging for testing.
     Initialize logging for testing.
 
 
@@ -35,7 +36,7 @@ class CBLogger(logging.Logger):
     Add a ``trace`` log level, numeric value 5: ``log.trace("Log message")``
     Add a ``trace`` log level, numeric value 5: ``log.trace("Log message")``
     """
     """
 
 
-    def trace(self, msg, *args, **kwargs):
+    def trace(self, msg: object, *args: object, **kwargs: Any) -> None:
         """Add ``trace`` log level."""
         """Add ``trace`` log level."""
         self.log(TRACE, msg, *args, **kwargs)
         self.log(TRACE, msg, *args, **kwargs)
 
 
@@ -61,7 +62,8 @@ log.addHandler(logging.NullHandler())
 #   cloudbridge.set_file_logger(__name__, '/tmp/log')
 #   cloudbridge.set_file_logger(__name__, '/tmp/log')
 
 
 
 
-def set_stream_logger(name, level=TRACE, format_string=None):
+def set_stream_logger(name: str, level: int = TRACE,
+                      format_string: str | None = None) -> None:
     """A convenience method to set the global logger to stream."""
     """A convenience method to set the global logger to stream."""
     global log
     global log
     if not format_string:
     if not format_string:
@@ -76,7 +78,8 @@ def set_stream_logger(name, level=TRACE, format_string=None):
     log = logger
     log = logger
 
 
 
 
-def set_file_logger(name, filepath, level=logging.INFO, format_string=None):
+def set_file_logger(name: str, filepath: str, level: int = logging.INFO,
+                    format_string: str | None = None) -> None:
     """A convenience method to set the global logger to a file."""
     """A convenience method to set the global logger to a file."""
     global log
     global log
     if not format_string:
     if not format_string:

+ 15 - 11
cloudbridge/factory.py

@@ -3,6 +3,8 @@ import inspect
 import logging
 import logging
 import pkgutil
 import pkgutil
 from collections import defaultdict
 from collections import defaultdict
+from typing import Any
+from typing import cast
 
 
 from cloudbridge import providers
 from cloudbridge import providers
 from cloudbridge.interfaces import CloudProvider
 from cloudbridge.interfaces import CloudProvider
@@ -26,11 +28,11 @@ class CloudProviderFactory(object):
     Get info and handle on the available cloud provider implementations.
     Get info and handle on the available cloud provider implementations.
     """
     """
 
 
-    def __init__(self):
-        self.provider_list = defaultdict(dict)
+    def __init__(self) -> None:
+        self.provider_list: defaultdict[str, dict[str, Any]] = defaultdict(dict)
         log.debug("Providers List: %s", self.provider_list)
         log.debug("Providers List: %s", self.provider_list)
 
 
-    def register_provider_class(self, cls):
+    def register_provider_class(self, cls: type) -> None:
         """
         """
         Registers a provider class with the factory. The class must
         Registers a provider class with the factory. The class must
         inherit from cloudbridge.interfaces.CloudProvider
         inherit from cloudbridge.interfaces.CloudProvider
@@ -61,7 +63,7 @@ class CloudProviderFactory(object):
             log.debug("Class: %s does not implement the CloudProvider"
             log.debug("Class: %s does not implement the CloudProvider"
                       "  interface. Ignoring...", cls)
                       "  interface. Ignoring...", cls)
 
 
-    def discover_providers(self):
+    def discover_providers(self) -> None:
         """
         """
         Discover all available providers within the
         Discover all available providers within the
         ``cloudbridge.providers`` package.
         ``cloudbridge.providers`` package.
@@ -74,7 +76,7 @@ class CloudProviderFactory(object):
             except Exception as e:
             except Exception as e:
                 log.debug("Could not import provider: %s", e)
                 log.debug("Could not import provider: %s", e)
 
 
-    def _import_provider(self, module_name):
+    def _import_provider(self, module_name: str) -> None:
         """
         """
         Imports and registers providers from the given module name.
         Imports and registers providers from the given module name.
         Raises an ImportError if the import does not succeed.
         Raises an ImportError if the import does not succeed.
@@ -88,7 +90,7 @@ class CloudProviderFactory(object):
             log.debug("Registering the provider: %s", cls)
             log.debug("Registering the provider: %s", cls)
             self.register_provider_class(cls)
             self.register_provider_class(cls)
 
 
-    def list_providers(self):
+    def list_providers(self) -> dict[str, dict[str, Any]]:
         """
         """
         Get a list of available providers.
         Get a list of available providers.
 
 
@@ -108,7 +110,8 @@ class CloudProviderFactory(object):
         log.debug("List of available providers: %s", self.provider_list)
         log.debug("List of available providers: %s", self.provider_list)
         return self.provider_list
         return self.provider_list
 
 
-    def create_provider(self, name, config):
+    def create_provider(self, name: str,
+                        config: dict[str, Any]) -> CloudProvider:
         """
         """
         Searches all available providers for a CloudProvider interface with the
         Searches all available providers for a CloudProvider interface with the
         given name, and instantiates it based on the given config dictionary,
         given name, and instantiates it based on the given config dictionary,
@@ -138,7 +141,7 @@ class CloudProviderFactory(object):
         log.debug("Created '%s' provider", name)
         log.debug("Created '%s' provider", name)
         return provider_class(config)
         return provider_class(config)
 
 
-    def get_provider_class(self, name):
+    def get_provider_class(self, name: str) -> type[CloudProvider] | None:
         """
         """
         Return a class for the requested provider.
         Return a class for the requested provider.
 
 
@@ -150,12 +153,13 @@ class CloudProviderFactory(object):
         impl = self.list_providers().get(name)
         impl = self.list_providers().get(name)
         if impl:
         if impl:
             log.debug("Returning provider class for %s", name)
             log.debug("Returning provider class for %s", name)
-            return impl["class"]
+            return cast("type[CloudProvider]", impl["class"])
         else:
         else:
             log.debug("Provider with the name: %s not found", name)
             log.debug("Provider with the name: %s not found", name)
             return None
             return None
 
 
-    def get_all_provider_classes(self, ignore_mocks=False):
+    def get_all_provider_classes(
+            self, ignore_mocks: bool = False) -> list[type[CloudProvider]]:
         """
         """
         Returns a list of classes for all available provider implementations
         Returns a list of classes for all available provider implementations
 
 
@@ -167,7 +171,7 @@ class CloudProviderFactory(object):
         :return: A list of all available provider classes or an empty list
         :return: A list of all available provider classes or an empty list
         if none found.
         if none found.
         """
         """
-        all_providers = []
+        all_providers: list[type[CloudProvider]] = []
         for impl in self.list_providers().values():
         for impl in self.list_providers().values():
             if ignore_mocks:
             if ignore_mocks:
                 if not issubclass(impl["class"], TestMockHelperMixin):
                 if not issubclass(impl["class"], TestMockHelperMixin):

+ 4 - 4
cloudbridge/interfaces/exceptions.py

@@ -54,7 +54,7 @@ class InvalidNameException(CloudBridgeBaseException):
     letters, which are not allowed in a resource name.
     letters, which are not allowed in a resource name.
     """
     """
 
 
-    def __init__(self, msg):
+    def __init__(self, msg: str) -> None:
         super(InvalidNameException, self).__init__(msg)
         super(InvalidNameException, self).__init__(msg)
 
 
 
 
@@ -68,7 +68,7 @@ class InvalidLabelException(InvalidNameException):
     identical.
     identical.
     """
     """
 
 
-    def __init__(self, msg):
+    def __init__(self, msg: str) -> None:
         super(InvalidLabelException, self).__init__(msg)
         super(InvalidLabelException, self).__init__(msg)
 
 
 
 
@@ -79,7 +79,7 @@ class InvalidValueException(CloudBridgeBaseException):
     direction of a firewall rule other than TrafficDirection.INBOUND or
     direction of a firewall rule other than TrafficDirection.INBOUND or
     TrafficDirection.OUTBOUND.
     TrafficDirection.OUTBOUND.
     """
     """
-    def __init__(self, param, value):
+    def __init__(self, param: str, value: object) -> None:
         super(InvalidValueException, self).__init__(
         super(InvalidValueException, self).__init__(
             "Param %s has been given an unrecognised value %s" %
             "Param %s has been given an unrecognised value %s" %
             (param, value))
             (param, value))
@@ -100,5 +100,5 @@ class InvalidParamException(InvalidNameException):
     to a service.find() method.
     to a service.find() method.
     """
     """
 
 
-    def __init__(self, msg):
+    def __init__(self, msg: str) -> None:
         super(InvalidParamException, self).__init__(msg)
         super(InvalidParamException, self).__init__(msg)

+ 33 - 18
cloudbridge/interfaces/provider.py

@@ -1,9 +1,24 @@
 """
 """
 Specification for a provider interface
 Specification for a provider interface
 """
 """
+from __future__ import annotations
+
 from abc import ABCMeta
 from abc import ABCMeta
 from abc import abstractmethod
 from abc import abstractmethod
 from abc import abstractproperty
 from abc import abstractproperty
+from typing import Any
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pyeventsystem.middleware import MiddlewareManager
+
+    from cloudbridge.interfaces.resources import Configuration
+    from cloudbridge.interfaces.resources import PlacementZone
+    from cloudbridge.interfaces.services import ComputeService
+    from cloudbridge.interfaces.services import DnsService
+    from cloudbridge.interfaces.services import NetworkingService
+    from cloudbridge.interfaces.services import SecurityService
+    from cloudbridge.interfaces.services import StorageService
 
 
 
 
 class CloudProvider(object):
 class CloudProvider(object):
@@ -13,7 +28,7 @@ class CloudProvider(object):
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def __init__(self, config):
+    def __init__(self, config: dict[str, Any]) -> None:
         """
         """
         Create a new provider instance given a dictionary of
         Create a new provider instance given a dictionary of
         configuration attributes.
         configuration attributes.
@@ -31,7 +46,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def config(self):
+    def config(self) -> Configuration:
         """
         """
         Returns the config object associated with this provider. This object
         Returns the config object associated with this provider. This object
         is a subclass of :class:`dict` and will contain the properties
         is a subclass of :class:`dict` and will contain the properties
@@ -58,7 +73,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def middleware(self):
+    def middleware(self) -> MiddlewareManager:
         """
         """
         Returns the middleware manager associated with this provider. The
         Returns the middleware manager associated with this provider. The
         middleware manager can be used to add or remove middleware from
         middleware manager can be used to add or remove middleware from
@@ -72,7 +87,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def clone(self, zone=None):
+    def clone(self, zone: PlacementZone | None = None) -> CloudProvider:
         """
         """
         Create a clone of this provider. An optional `zone` parameter can be
         Create a clone of this provider. An optional `zone` parameter can be
         used to clone the provider to use a different zone.
         used to clone the provider to use a different zone.
@@ -101,7 +116,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def authenticate(self):
+    def authenticate(self) -> bool:
         """
         """
         Checks whether a provider can be successfully authenticated with the
         Checks whether a provider can be successfully authenticated with the
         configured settings. Clients are *not* required to call this method
         configured settings. Clients are *not* required to call this method
@@ -127,7 +142,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def has_service(self, service_type):
+    def has_service(self, service_type: str) -> bool:
         """
         """
         Checks whether this provider supports a given service.
         Checks whether this provider supports a given service.
 
 
@@ -149,7 +164,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def region_name(self):
+    def region_name(self) -> str:
         """
         """
         Returns the region that this provider is connected to.
         Returns the region that this provider is connected to.
         All provider operations will take place within this region.
         All provider operations will take place within this region.
@@ -160,7 +175,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def zone_name(self):
+    def zone_name(self) -> str | None:
         """
         """
         Returns the placement zone that this provider is connected to.
         Returns the placement zone that this provider is connected to.
         All provider operations will take place within this zone. Placement
         All provider operations will take place within this zone. Placement
@@ -183,7 +198,7 @@ class CloudProvider(object):
 #         pass
 #         pass
 
 
     @abstractproperty
     @abstractproperty
-    def compute(self):
+    def compute(self) -> ComputeService:
         """
         """
         Provides access to all compute related services in this provider.
         Provides access to all compute related services in this provider.
 
 
@@ -206,7 +221,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def networking(self):
+    def networking(self) -> NetworkingService:
         """
         """
         Provide access to all network related services in this provider.
         Provide access to all network related services in this provider.
 
 
@@ -223,7 +238,7 @@ class CloudProvider(object):
         """
         """
 
 
     @abstractproperty
     @abstractproperty
-    def security(self):
+    def security(self) -> SecurityService:
         """
         """
         Provides access to key pair management and firewall control
         Provides access to key pair management and firewall control
 
 
@@ -241,7 +256,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def storage(self):
+    def storage(self) -> StorageService:
         """
         """
         Provides access to storage related services in this provider.
         Provides access to storage related services in this provider.
         This includes the volume, snapshot and bucket services,
         This includes the volume, snapshot and bucket services,
@@ -262,7 +277,7 @@ class CloudProvider(object):
         pass
         pass
 
 
     @abstractproperty
     @abstractproperty
-    def dns(self):
+    def dns(self) -> DnsService:
         """
         """
         Provides access to all DNS related services.
         Provides access to all DNS related services.
 
 
@@ -288,14 +303,14 @@ class TestMockHelperMixin(object):
     like HTTPretty which take over socket communications.
     like HTTPretty which take over socket communications.
     """
     """
 
 
-    def setUpMock(self):
+    def setUpMock(self) -> None:
         """
         """
         Called before a test is started.
         Called before a test is started.
         """
         """
         raise NotImplementedError(
         raise NotImplementedError(
             'TestMockHelperMixin.setUpMock not implemented')
             'TestMockHelperMixin.setUpMock not implemented')
 
 
-    def tearDownMock(self):
+    def tearDownMock(self) -> None:
         """
         """
         Called before test teardown.
         Called before test teardown.
         """
         """
@@ -312,11 +327,11 @@ class ContainerProvider(object):
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def create_container(self):
+    def create_container(self) -> Any:
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def delete_container(self):
+    def delete_container(self) -> Any:
         pass
         pass
 
 
 
 
@@ -328,7 +343,7 @@ class DeploymentProvider(object):
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def deploy(self, target):
+    def deploy(self, target: Any) -> Any:
         """
         """
         Deploys on given target, where target is an Instance or Container
         Deploys on given target, where target is an Instance or Container
         """
         """

Файловите разлики са ограничени, защото са твърде много
+ 144 - 116
cloudbridge/interfaces/resources.py


Файловите разлики са ограничени, защото са твърде много
+ 183 - 106
cloudbridge/interfaces/services.py


+ 57 - 34
cloudbridge/interfaces/subservices.py

@@ -1,17 +1,31 @@
+from __future__ import annotations
+
+import builtins
 from abc import ABCMeta
 from abc import ABCMeta
 from abc import abstractmethod
 from abc import abstractmethod
+from typing import Any
 
 
+from cloudbridge.interfaces.resources import BucketObject
+from cloudbridge.interfaces.resources import DnsRecord
+from cloudbridge.interfaces.resources import FloatingIP
+from cloudbridge.interfaces.resources import Gateway
+from cloudbridge.interfaces.resources import InternetGateway
 from cloudbridge.interfaces.resources import PageableObjectMixin
 from cloudbridge.interfaces.resources import PageableObjectMixin
+from cloudbridge.interfaces.resources import ResultList
+from cloudbridge.interfaces.resources import Subnet
+from cloudbridge.interfaces.resources import TrafficDirection
+from cloudbridge.interfaces.resources import VMFirewall
+from cloudbridge.interfaces.resources import VMFirewallRule
 
 
 
 
-class BucketObjectSubService(PageableObjectMixin):
+class BucketObjectSubService(PageableObjectMixin[BucketObject]):
     """
     """
     A container service for objects within a bucket.
     A container service for objects within a bucket.
     """
     """
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def get(self, name):
+    def get(self, name: str) -> BucketObject | None:
         """
         """
         Retrieve a given object from this bucket.
         Retrieve a given object from this bucket.
 
 
@@ -25,7 +39,8 @@ class BucketObjectSubService(PageableObjectMixin):
 
 
     @abstractmethod
     @abstractmethod
     # pylint:disable=arguments-differ
     # pylint:disable=arguments-differ
-    def list(self, limit=None, marker=None, prefix=None):
+    def list(self, limit: int | None = None, marker: str | None = None,
+             prefix: str | None = None) -> ResultList[BucketObject]:
         """
         """
         List objects in this bucket.
         List objects in this bucket.
 
 
@@ -44,7 +59,7 @@ class BucketObjectSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[BucketObject]:
         """
         """
         Search for an object by a given list of attributes.
         Search for an object by a given list of attributes.
 
 
@@ -62,7 +77,7 @@ class BucketObjectSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, name):
+    def create(self, name: str) -> BucketObject:
         """
         """
         Create a new object within this bucket.
         Create a new object within this bucket.
 
 
@@ -72,14 +87,14 @@ class BucketObjectSubService(PageableObjectMixin):
         pass
         pass
 
 
 
 
-class GatewaySubService(PageableObjectMixin):
+class GatewaySubService(PageableObjectMixin[InternetGateway]):
     """
     """
     Manage internet gateway resources.
     Manage internet gateway resources.
     """
     """
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def get_or_create(self):
+    def get_or_create(self) -> InternetGateway:
         """
         """
         Creates new or returns an existing internet gateway for a network.
         Creates new or returns an existing internet gateway for a network.
 
 
@@ -92,7 +107,7 @@ class GatewaySubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def delete(self, gateway):
+    def delete(self, gateway: Gateway) -> None:
         """
         """
         Delete a gateway.
         Delete a gateway.
 
 
@@ -102,7 +117,8 @@ class GatewaySubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[InternetGateway]:
         """
         """
         List all available internet gateways.
         List all available internet gateways.
 
 
@@ -112,14 +128,14 @@ class GatewaySubService(PageableObjectMixin):
         pass
         pass
 
 
 
 
-class FloatingIPSubService(PageableObjectMixin):
+class FloatingIPSubService(PageableObjectMixin[FloatingIP]):
     """
     """
     Base interface for a FloatingIP Service.
     Base interface for a FloatingIP Service.
     """
     """
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def get(self, fip_id):
+    def get(self, fip_id: str) -> FloatingIP | None:
         """
         """
         Returns a FloatingIP given its ID or ``None`` if not found.
         Returns a FloatingIP given its ID or ``None`` if not found.
 
 
@@ -132,7 +148,8 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[FloatingIP]:
         """
         """
         List floating (i.e., static) IP addresses.
         List floating (i.e., static) IP addresses.
 
 
@@ -142,7 +159,7 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[FloatingIP]:
         """
         """
         Searches for a FloatingIP by a given list of attributes.
         Searches for a FloatingIP by a given list of attributes.
 
 
@@ -162,7 +179,7 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self):
+    def create(self) -> FloatingIP:
         """
         """
         Allocate a new floating (i.e., static) IP address.
         Allocate a new floating (i.e., static) IP address.
 
 
@@ -172,7 +189,7 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def delete(self, fip_id):
+    def delete(self, fip_id: FloatingIP | str) -> None:
         """
         """
         Delete an existing FloatingIP.
         Delete an existing FloatingIP.
 
 
@@ -182,14 +199,14 @@ class FloatingIPSubService(PageableObjectMixin):
         pass
         pass
 
 
 
 
-class VMFirewallRuleSubService(PageableObjectMixin):
+class VMFirewallRuleSubService(PageableObjectMixin[VMFirewallRule]):
     """
     """
     Base interface for Firewall rules.
     Base interface for Firewall rules.
     """
     """
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def get(self, rule_id):
+    def get(self, rule_id: str) -> VMFirewallRule | None:
         """
         """
         Return a firewall rule given its ID.
         Return a firewall rule given its ID.
 
 
@@ -212,7 +229,8 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[VMFirewallRule]:
         """
         """
         List all firewall rules associated with this firewall.
         List all firewall rules associated with this firewall.
 
 
@@ -222,8 +240,10 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, direction, protocol=None, from_port=None,
-               to_port=None, cidr=None, src_dest_fw=None):
+    def create(self, direction: TrafficDirection, protocol: str | None = None,
+               from_port: int | None = None, to_port: int | None = None,
+               cidr: str | builtins.list[str] | None = None,
+               src_dest_fw: VMFirewall | None = None) -> VMFirewallRule:
         """
         """
         Create a VM firewall rule.
         Create a VM firewall rule.
 
 
@@ -274,7 +294,7 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[VMFirewallRule]:
         """
         """
         Find a firewall rule filtered by the given parameters.
         Find a firewall rule filtered by the given parameters.
 
 
@@ -310,7 +330,7 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def delete(self, rule_id):
+    def delete(self, rule_id: str) -> None:
         """
         """
         Delete an existing VMFirewall rule.
         Delete an existing VMFirewall rule.
 
 
@@ -320,14 +340,14 @@ class VMFirewallRuleSubService(PageableObjectMixin):
         pass
         pass
 
 
 
 
-class SubnetSubService(PageableObjectMixin):
+class SubnetSubService(PageableObjectMixin[Subnet]):
     """
     """
     Base interface for a Subnet Service.
     Base interface for a Subnet Service.
     """
     """
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def get(self, subnet_id):
+    def get(self, subnet_id: str) -> Subnet | None:
         """
         """
         Returns a Subnet given its ID or ``None`` if not found.
         Returns a Subnet given its ID or ``None`` if not found.
 
 
@@ -340,7 +360,8 @@ class SubnetSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[Subnet]:
         """
         """
         List subnets within the network holding this subservice.
         List subnets within the network holding this subservice.
 
 
@@ -350,7 +371,7 @@ class SubnetSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[Subnet]:
         """
         """
         Searches for a Subnet by a given list of attributes.
         Searches for a Subnet by a given list of attributes.
 
 
@@ -370,7 +391,7 @@ class SubnetSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, label, cidr_block):
+    def create(self, label: str, cidr_block: str) -> Subnet:
         """
         """
         Create a new subnet within the network holding this subservice.
         Create a new subnet within the network holding this subservice.
 
 
@@ -387,7 +408,7 @@ class SubnetSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def delete(self, subnet_id):
+    def delete(self, subnet_id: Subnet | str) -> None:
         """
         """
         Delete an existing Subnet.
         Delete an existing Subnet.
 
 
@@ -397,14 +418,14 @@ class SubnetSubService(PageableObjectMixin):
         pass
         pass
 
 
 
 
-class DnsRecordSubService(PageableObjectMixin):
+class DnsRecordSubService(PageableObjectMixin[DnsRecord]):
     """
     """
     Base interface for a Dns Record Service.
     Base interface for a Dns Record Service.
     """
     """
     __metaclass__ = ABCMeta
     __metaclass__ = ABCMeta
 
 
     @abstractmethod
     @abstractmethod
-    def get(self, record_id):
+    def get(self, record_id: str) -> DnsRecord | None:
         """
         """
         Returns a Dns Record given its ID or ``None`` if not found.
         Returns a Dns Record given its ID or ``None`` if not found.
 
 
@@ -417,7 +438,8 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def list(self, limit=None, marker=None):
+    def list(self, limit: int | None = None,
+             marker: str | None = None) -> ResultList[DnsRecord]:
         """
         """
         List Dns Records within the Dns Zone holding this subservice.
         List Dns Records within the Dns Zone holding this subservice.
 
 
@@ -427,7 +449,7 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def find(self, **kwargs):
+    def find(self, **kwargs: Any) -> ResultList[DnsRecord]:
         """
         """
         Searches for a DnsRecord by a given list of attributes.
         Searches for a DnsRecord by a given list of attributes.
 
 
@@ -447,7 +469,8 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def create(self, label, type, data, ttl=None):
+    def create(self, label: str, type: str, data: str,
+               ttl: int | None = None) -> DnsRecord:
         """
         """
         Create a new DnsRecord within the Dns Zone holding this subservice.
         Create a new DnsRecord within the Dns Zone holding this subservice.
 
 
@@ -469,7 +492,7 @@ class DnsRecordSubService(PageableObjectMixin):
         pass
         pass
 
 
     @abstractmethod
     @abstractmethod
-    def delete(self, record_id):
+    def delete(self, record_id: DnsRecord | str) -> None:
         """
         """
         Delete an existing DnsRecord.
         Delete an existing DnsRecord.
 
 

+ 0 - 0
cloudbridge/py.typed


+ 30 - 0
pyproject.toml

@@ -91,6 +91,7 @@ dev = [
     "pydevd",
     "pydevd",
     "flake8>=3.3.0",
     "flake8>=3.3.0",
     "flake8-import-order>=0.12",
     "flake8-import-order>=0.12",
+    "mypy>=1.11",
 ]
 ]
 
 
 [tool.setuptools.dynamic]
 [tool.setuptools.dynamic]
@@ -100,6 +101,10 @@ version = { attr = "cloudbridge.__version__" }
 include = ["cloudbridge*"]
 include = ["cloudbridge*"]
 exclude = ["tests*"]
 exclude = ["tests*"]
 
 
+[tool.setuptools.package-data]
+# Ship the PEP 561 marker so downstream consumers pick up our type hints.
+cloudbridge = ["py.typed"]
+
 [tool.coverage.run]
 [tool.coverage.run]
 branch = true
 branch = true
 source = ["cloudbridge"]
 source = ["cloudbridge"]
@@ -108,3 +113,28 @@ omit = [
     "cloudbridge/__init__.py",
     "cloudbridge/__init__.py",
 ]
 ]
 parallel = true
 parallel = true
+
+[tool.mypy]
+# CloudBridge is typed gradually. The public API (the interface layer and the
+# factory) is held to a strict baseline so downstream users get a fully-typed
+# API; the base implementations and providers (which wrap untyped cloud SDKs)
+# are temporarily exempted below and ratcheted to strict module-by-module.
+python_version = "3.13"
+files = ["cloudbridge"]
+ignore_missing_imports = true   # untyped cloud SDKs (boto3, azure-*, ...) -> Any
+namespace_packages = true
+warn_unused_configs = true
+# Strict baseline: applies to every module NOT exempted below.
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+no_implicit_optional = true
+check_untyped_defs = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_return_any = true
+
+# Gradual-typing exemptions. Remove a module from this list once it is fully
+# annotated; it then falls under the strict baseline above.
+[[tool.mypy.overrides]]
+module = ["cloudbridge.base.*", "cloudbridge.providers.*"]
+ignore_errors = true

+ 9 - 1
tox.ini

@@ -6,7 +6,7 @@
 # running the tests.
 # running the tests.
 
 
 [tox]
 [tox]
-envlist = py3.13-{aws,azure,gcp,openstack,mock},lint
+envlist = py3.13-{aws,azure,gcp,openstack,mock},lint,mypy
 
 
 [testenv]
 [testenv]
 commands = # see pyproject.toml for coverage options; setup.cfg for flake8
 commands = # see pyproject.toml for coverage options; setup.cfg for flake8
@@ -89,3 +89,11 @@ deps =
 [testenv:lint]
 [testenv:lint]
 commands = flake8 cloudbridge tests
 commands = flake8 cloudbridge tests
 deps = flake8
 deps = flake8
+
+[testenv:mypy]
+# Type-check the public interface layer (see [tool.mypy] in pyproject.toml).
+# The interfaces import no third-party packages, so the package itself does not
+# need installing here, keeping this env fast.
+skip_install = true
+deps = mypy
+commands = mypy

Някои файлове не бяха показани, защото твърде много файлове са промени