from pathlib import Path
from typing import Optional
from types import ModuleType, FunctionType


class Platform:
    pass




class Pipeline:
    """A pipeline is a procedure for building, testing, and deploying code and
    resources.

    A pipeline consists of a number of build targets. Build targets are linked
    to generated files. A build target can output files, and can read files
    generated by other buid targets, which introduces dependencies. Build steps
    can be short circuited for incremental builds if none of the input files
    change. The pipeline tracks input and output files of each build step to
    control invalidation.

    The pipeline has an async API for build parallelization. Build steps can be
    carried out in parallel on separate coroutines (for IO-bound build steps)
    or separate processes (for CPU-bound build steps).

    Deployments can be rollbacked back automatically in case of failure, or
    just for testing when a permanant deployment isn't necessary. The pipeline
    track deployed resources and runs through a context manager. Deployed
    resources are released when exiting the context if they are not released to
    a permanant deployment.

    """

    root_of_build_dir: Path
    package_name: str
    root_module: ModuleType

    def import_modules(self):
        """Import the sub-modules of the root_module.

        Importing modules ensures that everything the code is evaluated.

        Modules are imported by iterating through the package heirarchy
        starting from the root_node.

        """

    def assign_names(self):
        """Assign each imported value a name.

        The name is stable across reruns (assuming deterministic user code).

        The name can be accessed with the object_name method.

        """

    @staticmethod
    def build_target(writer_method: Optional[FunctionType] = None,
                     *,
                     cpu_bound: bool = False):
        """Wrap the provided method as a build target.

        Build targets benefit from in-memory and on-disk caching to prevent
        unnecessary builds. Dependancies between build targets are
        transparently generated.
        """

    async def wait_for_build_targets_to_complete(self):
        """Block until all currently running build targets complete.

        Failure of any build target will cause this method to also fail by
        raising an exception.

        """

    async def start_build_stage(self, stage_name: str):
        """Start a new build stage name.

        wait_for_targets_to_complete will be called to ensure all targets from
        the previous build stage have been successfully completed before the
        new stage is started.
        """

    @property
    def current_build_target(self):
        """Return the value of the current build_target.

        The build target is transferred through a context variable, and so may
        differ between different coroutines at the same time. This means it is
        stable throughout the lifetime of a call stack. This enables
        asynchronous builds.

        """

    def mark_file_as_input(self, path: Path):
        """Mark the file as a build input for the current_build_target."""

    def mark_file_as_output(self, path: Path):
        """Mark the file as a build output for the current_build_target.

        It may be a dependancy for other build targets.

        """

    async def write_text(self, path: Path, text: str):
        """Write the provided text to the given path.

        Also add the file as an output for the current_build_target.

        """

    async def read_text(self, path: Path) -> str:
        """Read the text from the given path.

        Also add the file as an input for the current_build_target.

        """

    def object_path(self, obj: object, suffix: str) -> Path:
        """An object's path is relative to the root module.

        Paths are assigned to objects by traversing through objects accessible
        from the root_module after required modules are imported.

        The path is a safe to embed in a URL or a file name on most file
        systems.

        """

    @build_target
    async def make_gherkin_file(self, obj: object):
        """The gherkin file contains a serialization of the object.

        A gherkin file may be dependant on other gherkin files for
        deserialization.

        Referenced code in included in the gherkin files, unlink pickle, so it
        can be deserialized without the code. This is benefical to build
        servers as they won't need the package code if the gherkin files are
        passed to the build server.

        Gherkin files are also smaller than pickle files, but requires that
        object attributes match their expected type's interface. In a way,
        gherkin is like pickle but strongly typed.

        Gherkin files allow a Python runtime to be stored on disk, where it can
        be loaded into another runtime, even on a different computer.

        Gherkin files are the root dependancy for subsequent build stages, so
        if they do not change, none of their dependencies need to change. This
        leads to faster build times.

        """

    @build_target
    async def make_cpp_source_file(self, obj: object):
        ...


    @build_target
    async def make_cpp_header_file(self, obj: object):
        ...


    @build_target
    async def make_object_file(self, obj: object, platform: Platform):
        """This may be carried out on a remote build server if the current
        platform does note have the toolchains for the platform installed.

        In this case, the object file will be stored on the build server, and
        only a small proxy fill will be written locally.

        """
        ...

    @build_target
    async def make_web_service_image(self, obj: object):
        ...

    @build_target
    async def make_web_page_static_assets(self, obj: object):
        ...

    @build_target
    async def make_firmware_image(self, obj: object):
        ...

    def deployment_context_manager():
        ...

    @deployment_context_manager
    async def web_page_deployment(obj: object):
        ...

    @deployment_context_manager
    async def web_service_deployment(obj: object):
        ...

    @deployment_context_manager
    async def firmware_deployment(obj: object):
        ...

    async def release():
        """Release the deployed resources in their current state.

        The asynchonous call will complete when the release is done.

        Deployed resources will not be torn down when their context exits after
        they have been released.

        This should generally be only called once per pipeline.

        """
