Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add base classes and docs #191

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/code_quality.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ jobs:
with:
os: ${{ job.os }}
python-version: '3.11'
poetry-install-options: "--only=types --no-root"
poetry-export-options: "--only=types"
poetry-install-options: "--only=main --only=types --no-root"
uniqueg marked this conversation as resolved.
Show resolved Hide resolved
poetry-export-options: "--only=main --only=types"

- name: Check types
run: poetry run mypy tesk/
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: Include in a style: PR

Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Package contents

.. toctree::
:maxdepth: 2
:caption: package
:caption: API reference
uniqueg marked this conversation as resolved.
Show resolved Hide resolved

pages/tesk/modules

Expand Down
11 changes: 9 additions & 2 deletions docs/source/pages/tesk/modules.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
tesk
TESK
====

.. toctree::
:maxdepth: 4
:caption: Services (filer and taskmaster)

tesk
tesk.services

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have we altered this? I see the parent model is still tesk?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

segregation in the sidebar of documentation.

.. toctree::
:maxdepth: 4
:caption: API

tesk.api
18 changes: 18 additions & 0 deletions docs/source/pages/tesk/tesk.api.ga4gh.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
tesk.api.ga4gh package
======================

Subpackages

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the difference between submodules and subpackages? Any reason why we have used subpackages here and submodules at all other places?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autogenerated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the docs are auto-generated - why do we version control them, then? Does this require you to build the docs before committing (and if so, what if you forget)? Couldn't you just auto-generate them as part of your docs deployment and leave them out of the CI? For packages I wrote and documented their APIs via Sphinx and RtD, I only kept the index.rst and configuration - the pages were then generated by RtD and were not under version control (see FOCA, for example).

Admittedly, the API docs I generated with Sphinx and RtD are pretty shitty 🤣 Still, do we really need these files here?

-----------

.. toctree::
:maxdepth: 4

tesk.api.ga4gh.tes

Module contents
---------------

.. automodule:: tesk.api.ga4gh
:members:
:undoc-members:
:show-inheritance:
21 changes: 21 additions & 0 deletions docs/source/pages/tesk/tesk.api.ga4gh.tes.base.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
tesk.api.ga4gh.tes.base package
===============================

Submodules
----------

tesk.api.ga4gh.tes.base.base\_tesk\_request module
--------------------------------------------------

.. automodule:: tesk.api.ga4gh.tes.base.base_tesk_request
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

.. automodule:: tesk.api.ga4gh.tes.base
:members:
:undoc-members:
:show-inheritance:
29 changes: 29 additions & 0 deletions docs/source/pages/tesk/tesk.api.ga4gh.tes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
tesk.api.ga4gh.tes package
==========================

Subpackages
-----------

.. toctree::
:maxdepth: 4

tesk.api.ga4gh.tes.base

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not include tesk.api.ga4gh.models here?

Copy link
Author

@JaeAeich JaeAeich Jun 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autogenerated


Submodules
----------

tesk.api.ga4gh.tes.controllers module
-------------------------------------

.. automodule:: tesk.api.ga4gh.tes.controllers
JaeAeich marked this conversation as resolved.
Show resolved Hide resolved
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

.. automodule:: tesk.api.ga4gh.tes
:members:
:undoc-members:
:show-inheritance:
18 changes: 18 additions & 0 deletions docs/source/pages/tesk/tesk.api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
tesk.api package
================

Subpackages
-----------

.. toctree::
:maxdepth: 4

tesk.api.ga4gh

Module contents
---------------

.. automodule:: tesk.api
:members:
:undoc-members:
:show-inheritance:
2 changes: 1 addition & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ ignore_missing_imports = True
[mypy-connexion.*]
ignore_missing_imports = True

[mypy-foca]
[mypy-foca.*]
uniqueg marked this conversation as resolved.
Show resolved Hide resolved

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember we fixed a few things here? Can you list the issues faced here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from foca.config.config_parser import ConfigParser
This throws:

tesk/tesk_app.py:9: error: Skipping analyzing "foca.config.config_parser": module is installed, but missing library stubs or py.typed marker  [import-untyped]
tesk/tesk_app.py:9: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-imports

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is an issue that should be solved in a different PR though. Or were these errors a result of code added in this PR exclusively? If so, keep it here.

ignore_missing_imports = True
13 changes: 12 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ types-botocore = "^1.0.2"
types-requests = "^2.31.0.20240406"
types-urllib3 = "^1.26.25.14"
types-werkzeug = "^1.0.9"
types-pyyaml = "^6.0.12.20240311"

[tool.poetry.scripts]
api = 'tesk.app:main'
Expand Down
1 change: 1 addition & 0 deletions tesk/api/ga4gh/tes/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package for base class for TESK API request."""
46 changes: 46 additions & 0 deletions tesk/api/ga4gh/tes/base/base_tesk_request.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose you are aware that Connexion validates requests and responses based on the OpenAPI schema without us having to write models? It's basically the main reason we use Connexion.

Admittedly, this predates the time of Pydantic - and I do like a good Pydantic model. However, this all may really not be necessary.

On another note, apart from your doc strings, I don't see anything TES-specific in here...

Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Base class for the TES API request."""

from abc import ABC, abstractmethod
from typing import Any, final

from pydantic import BaseModel

from tesk.tesk_app import TeskApp


class BaseTeskRequest(ABC, TeskApp):
"""Base class for the TES API.

This class is an abstract class that defines the common properties and
methods needed by all of the TES API endpoint business logic.
"""

def __init__(self) -> None:
"""Initializes the BaseTeskRequest class."""
super().__init__()

@abstractmethod
def api_response(self) -> BaseModel:
"""Returns the response as Pydantic model.

Should be implemented by the child class as final
business logic for the specific endpoint.

Returns:
BaseModel: API response for the specific endpoint.
"""
pass

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider throwing an exception here.


@final
def response(self) -> dict[Any, Any]:
JaeAeich marked this conversation as resolved.
Show resolved Hide resolved
"""Returns serialized response.

Should be invoked by controller.

Returns:
dict: Serialized response for the specific endpoint.
"""
_response = self.api_response()
if not isinstance(_response, BaseModel):
raise TypeError('API response must be a Pydantic model.')
return _response.dict()
1 change: 1 addition & 0 deletions tesk/api/ga4gh/tes/models/validators/base/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package for base class for custom pydantic validators."""
79 changes: 79 additions & 0 deletions tesk/api/ga4gh/tes/models/validators/base/base_validator.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why we don't just use Pydantic's tools for writing validators? What are your concrete use cases here?

Also, note that this may change for the next FOCA version, which will use Pydantic v2 - and validators work quite differently from v1.

Again

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Base validator class, all custom validators must implement it."""

import logging
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar

from pydantic import ValidationError

T = TypeVar('T')
logger = logging.getLogger(__name__)


class BaseValidator(ABC, Generic[T]):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need generic here? ABC should be sufficient IMO

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to give type annotations to the value of the field I am getting, because the validator in future can be for a complex datatype as well, in that case it would be good that we can have type hinting of what data str will the methods return.

"""Base custom validator class.

Validators assume that the filed being validated is not optional as

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This description is not apt. Please rephrase to keep it more informative to the users.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be including all possible being covered as part of the base validator.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check

optional fields are handled by the Pydantic model itself and mypy type
checking.
"""

@property
@abstractmethod
def error_message(self) -> str:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have this enforced and throw exception by default.

"""Returns the error message.

Returns:
str: The error message to be used when validation fails.
"""
pass

@abstractmethod
def validation_logic(self, v: T) -> bool:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would want to name it more intuitively and not v.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will the validators be created on the basis of field? If yes, then this makes sense. Else we should not be having field level naming conventions here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field level validator wouldn't be reusable in this case, so these will be property based validator.

"""Validation logic for the field.

Args:
v: The value being validated.

Returns:
bool: True if the validation is successful, False otherwise.
"""
pass

def _raise_error(self, cls: Any, v: T) -> None:
JaeAeich marked this conversation as resolved.
Show resolved Hide resolved
"""Raise a validation error.

Args:
cls: The class being validated.
v: The value being validated.

Raises:
ValidationError: Raised when the validation fails.
"""
logger.error(f'Validation failed for {v} in {cls.__name__}.')
raise ValidationError(
self.error_message,
model=cls,
)

def validate(self, cls: Any, v: T) -> T:
"""Validate the value.

If the value is None, ie the fields is optional
JaeAeich marked this conversation as resolved.
Show resolved Hide resolved
in the model, then it is returned as is without any validation.

Args:
cls: The class being validated.
v: The value being validated.

Returns:
T: The validated value (if valid).

Raises:
ValidationError: If the value is not valid.
"""
if not v:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this not violate the cause, pydantic is the authority for ensuring if null values are allowed or not. If we are writing validators underlying the same, should we not have a property to provide input if null is allowed or not?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition this logic is flawed,

Cases like : enforced arrays cannot be accounted here. Eg. if not [] will also satisfy the first condition.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we define at model level if the value is required or not, this will be ensured if not by pydantic then def by mypy. This means we should only care about writing validators for optional fields (as that would cover validation for required fields).

If a field is optional, the way pydantic validators are used is that we return value if its None.

if a model contains field as, my_array: [], in this case value is [] it is not None ie, its not optional in that case we sould validate it with the provided logic.

return v
elif not self.validation_logic(v):
self._raise_error(cls, v)
return v
47 changes: 7 additions & 40 deletions tesk/app.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,19 @@
"""API server entry point."""
"""App entry point."""

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned this because I plan to give tesk a command line tool like accessiblity, so app.py file would contain those logic this would be good as tesk would mostly be used in pods and interacted via commands.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did not get it, can you like some sort of document relevant to this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to use command pattern here, note that the tesk would be run in containers/pods and it should be make sense that it follows a standard cli pattern. Current if you see pyproject.toml we have two console script for tesk, ie filer and taskamster, which makes it look like they are two seperate binaries/scritps.

I think all three component should be launched with an argument (upto discussion this is just an example) as that will make more sense, with #179.

# base class to implement all three component
class Command:
    def execute(self):
        pass

# TeskAPP
class TeskApp(Command):
    def execute(self):
        print("Executing API command...")

# FilerApp
class FilerAPP(Command):
    def execute(self):
        print("Executing Filer command...")

# TaskmasterApp
class TaskmasterApp(Command):
    def execute(self):
        print("Executing Taskmaster command...")

# app.py
class CommandInvoker:
    def __init__(self):
        self._commands = {}

    def register(self, command_name, command):
        self._commands[command_name] = command

    def execute(self, command_name):
        if command_name in self._commands:
            self._commands[command_name].execute()
        else:
            print(f"No command registered for {command_name}")

# Usage
invoker = CommandInvoker()
invoker.register('--api', ApiCommand())
invoker.register('--filer', FilerCommand())
invoker.register('--taskmaster', TaskmasterCommand())

# Simulate CLI input
import sys
command_name = sys.argv[1] if len(sys.argv) > 1 else '--api'
invoker.execute(command_name)

What do you think @uniqueg @kushagra189 ??

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense, but would like to hear feedback also from @lvarin and/or @jemaltahir.

Besides that, I don't think that this change belongs in this PR, therefore I'm not reviewing details for now.

import logging
import os
from pathlib import Path

from connexion import FlaskApp
from foca import Foca
from tesk.tesk_app import TeskApp

logger = logging.getLogger(__name__)


def init_app() -> FlaskApp:
"""Initialize and return the FOCA app.

This function initializes the FOCA app by loading the configuration
from the environment variable `TESK_FOCA_CONFIG_PATH` if set, or from
the default path if not. It raises a `FileNotFoundError` if the
configuration file is not found.

Returns:
FlaskApp: A Connexion application instance.

Raises:
FileNotFoundError: If the configuration file is not found.
"""
# Determine the configuration path
config_path_env = os.getenv('TESK_FOCA_CONFIG_PATH')
if config_path_env:
config_path = Path(config_path_env).resolve()
else:
config_path = (
Path(__file__).parents[1] / 'deployment' / 'config.yaml'
).resolve()

# Check if the configuration file exists
if not config_path.exists():
raise FileNotFoundError(f'Config file not found at: {config_path}')

foca = Foca(
config_file=config_path,
)
return foca.create_app()


def main() -> None:
"""Run FOCA application."""
app = init_app()
app.run(port=app.port)
try:
TeskApp().run()
except Exception:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hope we have tested backward compatibility here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah :)

logger.exception('An error occurred while running the application.')
raise

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be raising appropriate runtime exception here.


if __name__ == '__main__':
Expand Down
Loading
Loading