Skip to content
This repository has been archived by the owner on Jan 14, 2024. It is now read-only.

Commit

Permalink
#66: Improve docs
Browse files Browse the repository at this point in the history
  • Loading branch information
blackandred committed Aug 29, 2021
1 parent 8836441 commit c491942
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 12 deletions.
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Keep learning
project-structure
standardlib/index
environment
writing-tasks
usage/index
rts/index

2 changes: 2 additions & 0 deletions docs/source/syntax.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.. _syntax:

Syntax
======

Expand Down
107 changes: 107 additions & 0 deletions docs/source/writing-tasks.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
Writing reusable tasks
======================

There are different ways to achieve similar goal, to define the Task. In chapter about :ref:`syntax` you can learn differences
between those multiple ways.

Now we will focus on **Classic Python** syntax which allows to define Tasks as classes, those classes can be packaged
into Python packages and reused across projects and event organizations.


Importing packages
------------------

Everytime a new project is created there is no need to duplicate same solutions over and over again.
Even in simplest makefiles there are ready-to-use tasks from :code:`rkd.core.standardlib` imported and used.

.. code:: yaml
version: org.riotkit.rkd/yaml/v2
imports:
- my_org.my_package1
Package index
-------------

A makefile can import a class or whole package. There is no any automatic class discovery, every package exports what was intended to export.

Below is explained how does it work that Makefile can import multiple tasks from :code:`my_org.my_package1` without specifying classes one-by-one.

**Example package structure**

.. code:: bash
my_package1/
my_package1/__init__.py
my_package1/script.py
my_package1/composer.py
**Example __init__.py inside Python package e.g. my_org.my_package1**

.. code:: python
from rkd.core.api.syntax import TaskDeclaration
from .composer import ComposerIntegrationTask # (1)
from .script import PhpScriptTask, imports as script_imports # (2)
# (3)
def imports():
return [
TaskDeclaration(ComposerIntegrationTask()) # (5)
] + script_imports() # (4)
- (1): **ComposerIntegrationTask** was imported from **composer.py** file
- (2): **imports as script_imports** other **def imports()** from **script.py** was loaded and used in **(4)**
- (3): **def imports()** defines which tasks will appear automatically in your build, when you import whole module, not a single class
- (5): **TaskDeclaration** can decide about custom task name, custom working directory, if the task is **internal** which means - if should be listed on :tasks


Task construction
-----------------

Basic example of how the Task looks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. literalinclude:: ../../src/core/rkd/core/standardlib/env.py
:start-after: <sphinx:getenv>
:end-before: # <sphinx:/getenv>

Basic configuration methods to implement
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- **get_name():** Define a name e.g. :code:`:my-task`
- **get_group_name():** Optionally a group name e.g. :code:`:app1`
- **get_declared_envs():** List of allowed environment variables to be used inside of this Task
- **configure_argparse():** Commandline switches configuration, uses Python's native ArgParse
- **get_configuration_attributes()**: Optionally. If our Task is designed to be used as Base Task of other Task, then there we can limit which methods and class attributes can be called from **configure()** method

.. autoclass:: rkd.core.api.contract.TaskInterface
:members: get_name, get_group_name, get_declared_envs, configure_argparse, get_configuration_attributes


Basic action methods
~~~~~~~~~~~~~~~~~~~~

- **execute():** Contains the Task logic, there is access to environment variables, commandline switches and class attributes
- **inner_execute():** If you want to create a Base Task, then implement a call to this method inside **execute()**, so the Task that extends your Base Task can inject code inside **execute()** you defined
- **configure():** If our Task extends other Task, then there is a possibility to configure Base Task in this method
- **compile():** Code that will execute on compilation stage. There is an access to **CompilationLifecycleEvent** which allows several operations such as **task expansion** (converting current task into a Pipeline with dynamically created Tasks)

.. autoclass:: rkd.core.api.contract.ExtendableTaskInterface
:members: execute, configure, compile, inner_execute


Additional methods that can be called inside execute() and inner_execute()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- **io():** Provides logging inside **execute()** and **configure()**
- **rkd() and sh():** Executes commands in subshells
- **py():** Executes Python code isolated in a subshell

.. autoclass:: rkd.core.api.contract.ExtendableTaskInterface
:members: io, rkd, sh, py

69 changes: 58 additions & 11 deletions src/core/rkd/core/api/contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,25 +320,31 @@ def copy_internal_dependencies(self, task):

@abstractmethod
def get_name(self) -> str:
"""Task name eg. ":sh"
"""
Task name eg. ":sh"
"""
pass

@abstractmethod
def get_group_name(self) -> str:
"""Group name where the task belongs eg. ":publishing", can be empty.
"""
Group name where the task belongs eg. ":publishing", can be empty.
"""

pass

def get_become_as(self) -> str:
"""User name in UNIX/Linux system, optional.
When defined, then current task will be executed as this user (WARNING: a forked process would be started)"""
"""
User name in UNIX/Linux system, optional.
When defined, then current task will be executed as this user (WARNING: a forked process would be started)
"""

return ''

def should_fork(self) -> bool:
"""Decides if task should be ran in a separate Python process (be careful with it)"""
"""
Decides if task should be ran in a separate Python process (be careful with it)
"""

return self.get_become_as() != ''

Expand All @@ -349,25 +355,51 @@ def get_description(self) -> str:

@abstractmethod
def execute(self, context: ExecutionContext) -> bool:
""" Executes a task. True/False should be returned as return """
"""
Executes a task. True/False should be returned as return
"""
pass

@abstractmethod
def configure_argparse(self, parser: ArgumentParser):
""" Allows a task to configure ArgumentParser (argparse) """
"""
Allows a task to configure ArgumentParser (argparse)
.. code:: python
def configure_argparse(self, parser: ArgumentParser):
parser.add_argument('--php', help='PHP version ("php" docker image tag)', default='8.0-alpine')
parser.add_argument('--image', help='Docker image name', default='php')
"""

pass

# ====== LIFECYCLE METHODS ENDS

def get_full_name(self):
""" Returns task full name, including group name """
"""
Returns task full name, including group name
"""

return self.get_group_name() + self.get_name()

@classmethod
def get_declared_envs(cls) -> Dict[str, Union[str, ArgumentEnv]]:
""" Dictionary of allowed envs to override: KEY -> DEFAULT VALUE """
"""
Dictionary of allowed envs to override: KEY -> DEFAULT VALUE
All environment variables fetched from the ExecutionContext needs to be defined there.
Declared values there are automatically documented in --help
.. code:: python
@classmethod
def get_declared_envs(cls) -> Dict[str, Union[str, ArgumentEnv]]:
return {
'PHP': ArgumentEnv('PHP', '--php', '8.0-alpine'),
'IMAGE': ArgumentEnv('IMAGE', '--image', 'php')
}
"""
return {}

def internal_normalized_get_declared_envs(self) -> Dict[str, ArgumentEnv]:
Expand Down Expand Up @@ -486,8 +518,6 @@ def silent_sh(self, cmd: str, verbose: bool = False, strict: bool = True,
return super().silent_sh(cmd=cmd, verbose=verbose, strict=strict, env=env)

def __str__(self):


return 'Task<{name}, object_id={id}, extended_from={extends}>'.format(
name=self.get_full_name(),
id=id(self),
Expand Down Expand Up @@ -531,6 +561,12 @@ def is_internal(self) -> bool:
return False

def extends_task(self):
"""
Provides information if this Task has a Parent Task
:return:
"""

try:
extends_from = self._extended_from.__module__ + '.' + self._extended_from.__name__
except AttributeError:
Expand All @@ -551,6 +587,17 @@ def __deepcopy__(self, memodict={}):

class ExtendableTaskInterface(TaskInterface, ABC):
def inner_execute(self, ctx: ExecutionContext) -> bool:
"""
Method that can be executed inside execute() - if implemented.
Use cases:
- Allow child Task to inject code between e.g. database startup and database shutdown to execute some
operations on the database
:param ctx:
:return:
"""

pass

def get_configuration_attributes(self) -> List[str]:
Expand Down
3 changes: 2 additions & 1 deletion src/core/rkd/core/standardlib/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ..api.contract import TaskInterface, ExecutionContext


# <sphinx:getenv>
class GetEnvTask(TaskInterface):
"""Gets environment variable value"""

Expand All @@ -19,10 +20,10 @@ def configure_argparse(self, parser: ArgumentParser):
parser.add_argument('--name', '-e', help='Environment variable name', required=True)

def execute(self, context: ExecutionContext) -> bool:
# @todo: test for case, when None then ''
self.io().out(os.getenv(context.get_arg('--name'), ''))

return True
# <sphinx:/getenv>


class SetEnvTask(TaskInterface):
Expand Down

0 comments on commit c491942

Please sign in to comment.