Skip to content

Commit

Permalink
v0.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
frabarz committed Mar 11, 2022
1 parent 7d52e9b commit c1dd11e
Show file tree
Hide file tree
Showing 25 changed files with 1,754 additions and 74 deletions.
11 changes: 11 additions & 0 deletions .editorconfig
@@ -0,0 +1,11 @@
root = true

[*]
end_of_line = lf
charset = utf-8

[*.py]
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
2 changes: 0 additions & 2 deletions .env

This file was deleted.

13 changes: 8 additions & 5 deletions .gitignore
@@ -1,8 +1,11 @@
__pycache__/
.pytest_cache/
.venv/
.vscode/
*.egg-info/
build/
dist/
venv/

.env
*.pyc
__pycache__/

dist/
build/
*.egg-info/
21 changes: 21 additions & 0 deletions LICENSE
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 Datawheel, LLC.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
44 changes: 44 additions & 0 deletions PACKAGE.md
@@ -0,0 +1,44 @@
<a href="https://github.com/Datawheel/logiclayer"><img src="https://flat.badgen.net/github/release/Datawheel/logiclayer" /></a>
<a href="https://github.com/Datawheel/logiclayer/blob/master/LICENSE"><img src="https://flat.badgen.net/github/license/Datawheel/logiclayer" /></a>
<a href="https://github.com/Datawheel/logiclayer/issues"><img src="https://flat.badgen.net/github/issues/Datawheel/logiclayer" /></a>


> A simple framework to quickly compose and use multiple functionalities as endpoints.
LogicLayer is built upon FastAPI to provide a simple way to group functionalities into reusable modules.

## Usage

To generate a new instance of LogicLayer, create a python file and execute this snippet:

```python
# example.py

import requests
from logiclayer import LogicLayer
from logiclayer.echo import EchoModule # Example module

echo = EchoModule()

def is_online() -> bool:
res = requests.get("http://clients3.google.com/generate_204")
return (res.status_code == 204) and (res.headers.get("Content-Length") == "0")

layer = LogicLayer()
layer.add_check(is_online)
layer.add_module(echo, prefix="/echo")
```

The `layer` object is an ASGI-compatible application, that can be used with uvicorn/gunicorn to run a server, the same way as you would with a FastAPI instance.

```bash
$ pip install uvicorn[standard]
$ uvicorn example:layer
```

> Note: The `example:layer` parameter refers to `full.module.path:asgi_object`, and will change according to how you set the file.
## License

&copy; 2022 Datawheel, LLC.
This project is licensed under [MIT](./LICENSE).
36 changes: 24 additions & 12 deletions README.md
@@ -1,28 +1,40 @@
# logiclayer
# LogicLayer

The missing piece for all your data processing needs.
> A simple framework to quickly compose and use multiple functionalities as endpoints.
<a href=""><img src="https://flat.badgen.net/github/license/Datawheel/logiclayer" /></a>
<a href=""><img src="https://flat.badgen.net/github/issues/Datawheel/logiclayer" /></a>
<a href=""><img src="https://flat.badgen.net/pypi/v/logiclayer" /></a>

## Installation

### Development environment
This package is available in PyPI under the name `logiclayer`. You can use `pip` or `poetry` to use it in your project:

```bash
pip install logiclayer
```

## Development environment

Create a virtual environment and install all the requirements.
To manage its dependencies, this project uses `poetry`. The [pyproject.toml](pyproject.toml) file contains all the needed dependencies an devDependencies needed. To install just run:

```bash
$ python3 -m venv venv
$ . venv/bin/activate
(venv)$ pip install -r requirements.txt
(venv)$ pip install -r dev-requirements.txt
$ poetry install
```

### Production environment
### Use with VSCode + Pylance

If you intend to use Visual Studio Code to work on this project, make sure poetry creates the virtual environment within the project folder, so VSCode can find the virtual environment. To use this mode run *before* the `install` command:

TODO
```bash
$ poetry config virtualenvs.in-project true
```

## Development guidelines

Please read the [design docs](docs/DESIGN.md) before you start.
Please read the [design docs](docs/DESIGN.md) before doing contributions.

## License

TODO
&copy; 2022 Datawheel, LLC.
This project is licensed under [MIT](./LICENSE).
36 changes: 36 additions & 0 deletions demo/__init__.py
@@ -0,0 +1,36 @@
import httpx

from logiclayer import LogicLayer
from logiclayer import __version__ as logiclayer_version
from logiclayer_olap import OlapModule
from logiclayer_geoservice import GeoserviceModule


# DEFINE A CHECK
def online_check():
res = httpx.get("http://clients3.google.com/generate_204")
return (res.status_code == 204) and (res.headers.get("Content-Length") == 0)


# DEFINE A SIMPLE ROUTE
def status_route():
return {"status": "ok", "software": "LogicLayer", "version": logiclayer_version}


# DEFINE A MODULE INSTANCE
geoservice = GeoserviceModule(schema="./geoservice.xml",
server="postgresql://user:pass@localhost:5432/mexico_geo")

olap = OlapModule("https://api.oec.world/tesseract/")


def run():
# CREATE A LOGICLAYER INSTANCE
layer = LogicLayer()

# ADD PLUGINS
layer.add_check(online_check)
layer.add_route("/", status_route)
layer.add_module(olap, prefix="/tesseract")

return layer
21 changes: 21 additions & 0 deletions demo/logging.ini
@@ -0,0 +1,21 @@
[loggers]
keys=root

[handlers]
keys=fileHandler

[formatters]
keys=simpleFormatter

[logger_root]
level=DEBUG
handlers=fileHandler

[handler_fileHandler]
class=FileHandler
level=DEBUG
formatter=simpleFormatter
args=("logiclayer.log",)

[formatter_simpleFormatter]
format=%(asctime)s %(name)s - %(levelname)s:%(message)s
3 changes: 0 additions & 3 deletions dev-requirements.txt

This file was deleted.

19 changes: 14 additions & 5 deletions docs/DESIGN.md
Expand Up @@ -8,7 +8,7 @@ As a framework, the main function is supply the tools to ease the connection bet

The core LogicLayer project should not implement neither datasource connections nor processing libraries (hereby "calculations") by itself. The calculations should be supplied as extension modules, and implemented following the instructions, and using the tools, provided by the core LogicLayer project. The project where LogicLayer is deployed should be responsible of the installation and implementation of the calculation modules, so only the required functionality is available.

As an example, a LogicLayer module for economic complexity might depend on the core LogicLayer modules that can retrieve the data from tesseract-olap, convert that data into a pandas dataframe, pass the dataframe into a module with functions that calculate economic complexity, and return these results to the user as JSON. The core LogicLayer framework should make sure every part is executed succesfully, and report any error.
As an example, a LogicLayer module for economic complexity might depend on the core LogicLayer modules that can retrieve the data from tesseract-olap, convert that data into a pandas dataframe, pass the dataframe into a module with functions that calculate economic complexity, and return these results to the user as JSON. The core LogicLayer framework should make sure every part is executed successfully, or/and report any exception raised during execution.

## Code Guidelines

Expand All @@ -22,16 +22,25 @@ Since we could need any kind of data processing library not only on the module i
It's ok to pack these functions on their own modules if there's an assumption it can be reused by other modules in the future.

- All data processing functions should accomplish the minimum objective possible.
- The parameters passed to these functions must be only data structures, and should not be related with the web framework.
- The parameters passed to these functions must be only primitives and data structures, and should not be related to the web framework.
- All functions must be documented appropiately, including type, shape, and expected return. This is a library that relies heavily on academic knowledge, and not all developers may be familiar with the theory.
- All functions should also raise Exceptions if there's a deviation of the normal behavior, eg. if the parameters don't have the same size. If some parts of the code can raise their own exceptions, do not catch them.
- All functions should be have their own test suites. The tests should attempt to reach successful returns and raising intended exceptions.
- All functions should also raise Exceptions if there's a deviation of the normal behavior, eg. if the parameters don't have the same size. Remember to document the Exceptions created for this purpose.
- If other libraries or some parts of the code can raise their own exceptions, catch them only if they're relevant to the internals of the function they're contained in. When catching exceptions, try to be as specific as possible. DO NOT catch using `Exception`.
- All functions should be have their own test suites. The tests should attempt both to reach successful returns, and raise intended exceptions.

### Web framework

- Web endpoints should are in charge of receiving parameters, parsing/transforming them, use them to execute the needed calculation functions, and chain their input/output as needed.
- Web endpoints should be in charge of receiving parameters, parsing/transforming them, use them to execute the needed calculation functions, and chain their input/output as needed.
- Adding comments to the execution chain is encouraged.
- If any function can raise an Exception, the whole execution chain must be contained in a `try/except` block.
- Each Route must ultimately return a `Response` instance, with proper response headers. For Flask, the `jsonify` function lets the user do this easily.
- Appropiate HTTP response codes are encouraged. If the execution is successful, a normal code 200 is implied in the jsonify function, but otherwise the proper 400/500 code should be set on the `Response` instance.
- Use the appropiate debug level when handling errors to return information about the problem, or just saying "Internal Server Error".

### Suggested procedure

The procedure to organize the code in a new module should follow these steps:

- DESIGN the functionality you want to implement. Think, group, and organize similar functionality.
- WRITE TESTS before coding, based on the design guidelines you reached. This way you can define what to expect and what not to do. You're still on time to change your design as you go.
- WRITE CODE to accomplish what you designed. Be a kind programmer and do not write code to specifically pass the tests. Always think in a general way; if one day your code is made into a module of its own, the refactor will be a breeze.
10 changes: 10 additions & 0 deletions docs/ROADMAP.md
@@ -0,0 +1,10 @@
# Feature draft

* Log all requests using `app.logger` on their applicable levels
This is an instance of python's logging.Logger class, so check documentation on how to implement custom targets:
* https://www.datadoghq.com/blog/python-logging-best-practices/
* https://flask.palletsprojects.com/en/1.1.x/quickstart/#logging

* Implement a JWT middleware to authenticate and control permissions
This would probably require tweaking the routes themselves
The olap-proxy module would also have to pass the header
11 changes: 11 additions & 0 deletions logiclayer/__init__.py
@@ -1,2 +1,13 @@
"""LogicLayer module.
"""

from .logiclayer import LogicLayer
from .module import LogicLayerModule

__all__ = (
"LogicLayer",
"LogicLayerModule",
)

__version_info__ = ('0', '1', '0')
__version__ = '.'.join(__version_info__)
22 changes: 22 additions & 0 deletions logiclayer/echo.py
@@ -0,0 +1,22 @@
from fastapi.params import Query

from .module import LogicLayerModule


class EchoModule(LogicLayerModule):
"""Echo Module for LogicLayer
This is just a test module, to check basic functionality.
"""

data = "eyJlbmNvZGluZyI6ImJhc2U2NCJ9"

def setup(self, router):

@router.get("/")
def route_index(message: str = Query(..., alias="msg")):
return {
"status": "ok",
"data": self.data,
"echo": message
}
13 changes: 13 additions & 0 deletions logiclayer/exceptions.py
@@ -0,0 +1,13 @@
"""LogicLayer errors and exceptions module.
Contains the errors and exceptions the code can raise at some point during
execution.
"""


class BaseError(Exception):
"""Base Error class for all errors in the module."""


class HealthCheckError(BaseError):
"""At least one of the healthchecks set in the LogicLayer instance failed."""
9 changes: 9 additions & 0 deletions logiclayer/logging.py
@@ -0,0 +1,9 @@
import logging
import logging.config
import os

# https://www.datadoghq.com/blog/python-logging-best-practices/
config_filepath = os.environ.get("LOGICLAYER_LOGGING_CONFIG", "logging.ini")
logging.config.fileConfig(config_filepath, disable_existing_loggers=False)

logger = logging.getLogger("logiclayer")

0 comments on commit c1dd11e

Please sign in to comment.