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

Overhaul structure and functionality #43

Open
wants to merge 96 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 49 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
7eeeeb7
refactor: Reorganize entire project structure
DavisRayM Jun 8, 2023
aea855d
refactor(url): remove extra `api/v1` prefix
DavisRayM Jun 8, 2023
296814b
refactor(models): ensure `relationship` points to actual models
DavisRayM Jun 8, 2023
2d1feb4
refactor(server): use CRUDServer class instead of model
DavisRayM Jun 8, 2023
1afa37a
lint: run formatter on files
DavisRayM Jun 8, 2023
3856443
refactor: return request and session ID
DavisRayM Jun 8, 2023
03a91ac
refactor(api-helper): Add `OnaDataAPIClient` class
DavisRayM Jun 8, 2023
b8974ad
refactor(auth): Use `get_current_user` for retrieving authenticated user
DavisRayM Jun 8, 2023
fc328d2
refactor(configuration): Use CRUD class instead of model
DavisRayM Jun 8, 2023
a38f8a5
refactor(config): Add `DOWNLOAD_EXPIRE_SECONDS` config
DavisRayM Jun 8, 2023
19065b2
refactor(schema): add missing fields to `FileCreate` schema
DavisRayM Jun 8, 2023
2c2f4d4
feat(crud-hyper): add `create` and get helper functions
DavisRayM Jun 8, 2023
d6953e3
feat(helpers): Add `Importer` helper class for CSV -> Hyper imports
DavisRayM Jun 8, 2023
cabc901
feat(onadata): add functions to help with downloading exports
DavisRayM Jun 8, 2023
6540e42
refactor(crud-user): Add helper functions to help with upstream syncing
DavisRayM Jun 8, 2023
09c98a7
refactor(main): initialize logging config
DavisRayM Jun 8, 2023
8490751
refactor(schema): Update `FileListItem` schema
DavisRayM Jun 8, 2023
0468aca
refactor(file-api): decouple and reorganize file routes
DavisRayM Jun 8, 2023
953b579
refactor(sync): re-introduce hyperfile syncing functionality
DavisRayM Jun 9, 2023
f738c19
chore: update logger unique ID
DavisRayM Jun 9, 2023
874974d
refactor(sync): ensure file syncing works as expected
DavisRayM Jun 13, 2023
e493d17
chore(deprecation): Add `DEPRECATED` comment on unused files
DavisRayM Jun 13, 2023
9108f98
chore(unused module): remove unused module
DavisRayM Jun 13, 2023
ce4f77b
refactor(url): ensure server URLs are cleaned
DavisRayM Jun 13, 2023
8dd65d0
refactor(build): update used docker file
DavisRayM Jun 13, 2023
20a355e
chore(tox): ensure `sh` is allowed
DavisRayM Jun 13, 2023
c50b3f7
refactor(test): update default settings
DavisRayM Jun 13, 2023
96469dd
refactor(settings): remove unused settings file
DavisRayM Jun 13, 2023
ba360e2
refactor(config): remove unused configuration
DavisRayM Jun 13, 2023
c4b353e
refactor(security): include expire only when it's set
DavisRayM Jun 13, 2023
8be802b
refactor(tableau): use `fernet_decrypt` instead of Configuration model
DavisRayM Jun 13, 2023
a6e9248
refactor(crud-user): include `server_id` in function arguements
DavisRayM Jun 13, 2023
a5fdecb
refactor(file-endpoint): ensure configuration is validated
DavisRayM Jun 13, 2023
f66fe81
refactor(crud-configuration): ensure updates work as expected
DavisRayM Jun 13, 2023
3349312
refactor(crud-hyperfile): ensure files are deleted from S3 after loca…
DavisRayM Jun 13, 2023
43f0dec
refactor(test-base): Update base testing classes
DavisRayM Jun 13, 2023
e867dde
refactor(tests): move files into correct folder
DavisRayM Jun 13, 2023
d9c7b2e
deprecate(utils): remove unused util tests
DavisRayM Jun 13, 2023
c260267
refactor(file): add `sync` endpoint
DavisRayM Jun 13, 2023
bb0ab54
refactor(crud-configuration): capture exact error
DavisRayM Jun 13, 2023
1552e59
refactor(imports): sort imports
DavisRayM Jun 13, 2023
16e6bd8
refactor(core): Add docstring for new classes
DavisRayM Jun 13, 2023
c264259
linting(app): remove unused imports
DavisRayM Jun 13, 2023
e8b2b58
docs(FAQ): add FAQ section
DavisRayM Jun 13, 2023
825fad7
chore(docker): update docker file
DavisRayM Jun 14, 2023
20561ce
refactor(router): allow trailing slashes
DavisRayM Jun 14, 2023
d4dd649
refactor(status-code): raise 401 instead of 403,404
DavisRayM Jun 14, 2023
991b6ae
lint: format files
DavisRayM Jun 14, 2023
c295379
refactor(auth): ensure access_token is refreshed
DavisRayM Jun 14, 2023
458e15f
code cleanup
KipSigei Jun 29, 2023
a67ea5c
Add missing __init__.py
KipSigei Jun 29, 2023
3665300
Fix token decryption bug
KipSigei Jul 4, 2023
554fec1
Push latest file to tableau
KipSigei Jul 4, 2023
cdd032c
Cleanup
KipSigei Sep 4, 2023
4642d64
Add python3.10 env
KipSigei Sep 4, 2023
fc3684e
Update dependencies, fix failing tests
KipSigei Jul 18, 2024
01d3b5e
Cleanup
KipSigei Jul 18, 2024
e6c7912
Update CI to use python 3.10
KipSigei Jul 18, 2024
e02ca6f
Remove python3.9 pipeline
KipSigei Jul 18, 2024
d70be69
Fix pydantic warnings
KipSigei Jul 18, 2024
dac82d4
Fix worker redis connection
KipSigei Jul 18, 2024
fb424eb
Fix data types
KipSigei Jul 23, 2024
e985026
The `from_orm` method is deprecated; use model_validate
KipSigei Jul 23, 2024
b78612c
Update logging configs
KipSigei Jul 23, 2024
04e809d
Enable debug mode and fix deprecation warnings
KipSigei Jul 23, 2024
8b98bb1
Fox pydantic validation errors
KipSigei Jul 23, 2024
ead7162
Update lifecycle handlers
KipSigei Aug 13, 2024
ff64342
[fix] Black reformatting
ukanga Sep 16, 2024
9fa59f7
fix(importer): DataFrame dtypes is of type Series
ukanga Sep 16, 2024
3eba870
feat(file): Redirect to download URL when .hyper extension is present
ukanga Sep 18, 2024
6e531cf
feat(file): Add download_url to the file list
ukanga Sep 18, 2024
6f52e3c
fix: Ensure the api prefix url is included in the files url/download_url
ukanga Sep 18, 2024
420115d
refactor(app/core): move FailedExternalRequest to remove a circular i…
ukanga Sep 19, 2024
f459fcb
refactor(app): remove onadata api query check from the hyperfile crud…
ukanga Sep 19, 2024
bf94f6d
chore: Add logging to files endpoint
ukanga Sep 24, 2024
97db8ed
refactor: use request.url.scheme
ukanga Sep 24, 2024
df28bd0
chore: Set MEDIA_ROOT from environment variable
ukanga Sep 24, 2024
b01d8c4
refactor: Use python:3.10 image for docker build
ukanga Sep 24, 2024
3184ad3
refactor: expect only integers for form_id and user_id in get_using_form
ukanga Sep 24, 2024
1d86249
chore(files): log request headers
ukanga Sep 24, 2024
2217c80
chore(files): log request scope
ukanga Sep 24, 2024
d7579b1
refactor: pick URL scheme from x-forwarded-proto headers
ukanga Sep 24, 2024
f0fcc61
chore: validate form_id on files endpoint
ukanga Sep 24, 2024
f339f24
Additional user logging for debugging purposes
ukanga Nov 19, 2024
71f4ffe
more logs
ukanga Nov 19, 2024
5f94ebe
chore: do not delete records being updated
ukanga Nov 22, 2024
cfdf924
chore: follow_redirects = True on CSV download
ukanga Nov 22, 2024
cb5bb9f
chore: logging cleanup
ukanga Nov 22, 2024
e8b319b
chore: raise FailedExternalRequest on export download failures
ukanga Dec 5, 2024
a0a9385
chore: handle FailedExternalRequest exception on import csv
ukanga Dec 5, 2024
e1c7105
fix: handle the case when the DB record for the Hyperfile does not exist
ukanga Dec 10, 2024
3d5e68b
chore: handle HyperExceptions when creating a HyperFile
ukanga Jan 7, 2025
9a0a5e0
chore: ensure MEDIA_ROOT DIR is created on rq worker startup
ukanga Jan 8, 2025
b6fc571
chore: Updater docker image build workflow
ukanga Jan 9, 2025
d6bbd49
Revert "chore: Updater docker image build workflow"
ukanga Jan 9, 2025
fa92697
chore: ensure MEDIA_ROOT DIR is created on rq worker startup
ukanga Jan 9, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/docker-hub-image-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile.prod
file: ./Dockerfile
platforms: linux/amd64
build-args: |
release_version=${{ github.event.inputs.versionTag || steps.get_version.outputs.VERSION }}
Expand Down
13 changes: 10 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.9

RUN mkdir -p /root/.aws
COPY . /app
WORKDIR /app/

COPY ./requirements.pip /app/requirements.pip
COPY ./dev-requirements.pip /app/dev-requirements.pip
RUN pip install -r requirements.pip

RUN mkdir -p /app/media && pip install --no-cache-dir -r /app/requirements.pip
ARG INSTALL_DEV=false
RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then pip install -r dev-requirements.pip ; fi"

COPY . /app
ENV PYTHONPATH=/app
20 changes: 0 additions & 20 deletions Dockerfile.prod

This file was deleted.

14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,17 @@ $ ./scripts/run-tests.sh
```sh
$ PYTHONPATH=. pytest -s app/tests
```

### FAQ

1. How do I access the shell for the application ?

> The application shell can be accessed via `python3`. You can import the crud module to perform any C.R.U.D(Create, Read, Update & Delete) actions on the models

2. What handles the authentication ?

> Authentication is handled via the functions in `app/api/auth_deps.py`. The `onadata` module contains a helper class used to keep OAuth2 Credentials valid.

3. How do I go about adding an extra field in responses ?

> All responses returned by the application are managed using pydantic schemas. In case you'd like to modify a response ensure the field exists in the model and update the `schemas` module
42 changes: 31 additions & 11 deletions app/alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import with_statement

import os
from logging.config import fileConfig

from alembic import context
from sqlalchemy import engine_from_config, pool

from app.models import Base
from app.settings import settings

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
Expand All @@ -14,10 +14,29 @@
# This line sets up loggers basically.
fileConfig(config.config_file_name)

config.set_main_option("sqlalchemy.url", settings.database_url)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# target_metadata = None

from app.database.base import Base # noqa

target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def get_url():
user = os.getenv("POSTGRES_USER", "duva")
password = os.getenv("POSTGRES_PASSWORD", "duva")
server = os.getenv("POSTGRES_SERVER", "postgres")
db = os.getenv("POSTGRES_DB", "duva")
return f"postgresql://{user}:{password}@{server}/{db}"


def run_migrations_offline():
"""Run migrations in 'offline' mode.
Expand All @@ -31,12 +50,9 @@ def run_migrations_offline():
script output.

"""
url = config.get_main_option("sqlalchemy.url")
url = get_url()
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True
)

with context.begin_transaction():
Expand All @@ -50,14 +66,18 @@ def run_migrations_online():
and associate a connection with the context.

"""
configuration = config.get_section(config.config_ini_section)
configuration["sqlalchemy.url"] = get_url()
connectable = engine_from_config(
config.get_section(config.config_ini_section),
configuration,
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection, target_metadata=target_metadata, compare_type=True
)

with context.begin_transaction():
context.run_migrations()
Expand Down
29 changes: 29 additions & 0 deletions app/alembic/versions/7e33515949c2_add_access_token_column.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""add access_token column

Revision ID: 7e33515949c2
Revises: dcbb9d699ca4
Create Date: 2023-06-08 10:10:58.240323

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "7e33515949c2"
down_revision = "dcbb9d699ca4"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("server", sa.Column("configuration", sa.JSON(), nullable=True))
op.add_column("user", sa.Column("access_token", sa.String(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("user", "access_token")
op.drop_column("server", "configuration")
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion app/alembic/versions/8a3e2f1927b8_add_file_status_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import sqlalchemy as sa
from alembic import op

from app.models import ChoiceType, schemas
from app.models.hyperfile import ChoiceType, schemas

# revision identifiers, used by Alembic.
revision = "8a3e2f1927b8"
Expand Down
42 changes: 42 additions & 0 deletions app/alembic/versions/dcbb9d699ca4_reorganize_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Reorganize models

Revision ID: dcbb9d699ca4
Revises: 259f90f13bd6
Create Date: 2023-05-31 11:57:11.230587

"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "dcbb9d699ca4"
down_revision = "259f90f13bd6"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("hyper_file", "last_synced")
op.create_index(op.f("ix_server_id"), "server", ["id"], unique=False)
op.alter_column("configuration", "user", new_column_name="user_id")
op.alter_column("hyper_file", "user", new_column_name="user_id")
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_server_id"), table_name="server")
op.add_column(
"hyper_file",
sa.Column(
"last_synced",
postgresql.TIMESTAMP(),
autoincrement=False,
nullable=True,
),
)
op.alter_column("hyper_file", "user_id", new_column_name="user")
op.alter_column("configuration", "user_id", new_column_name="user")
# ### end Alembic commands ###
File renamed without changes.
43 changes: 43 additions & 0 deletions app/api/auth_deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import jwt
from fastapi import Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session

from app import crud
from app.api.deps import get_db
from app.core.config import settings
from app.models.user import User
from app.schemas.token import TokenPayload

reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/oauth/login", auto_error=False)


def get_current_user(
db: Session = Depends(get_db),
token: str = Depends(reusable_oauth2),
*,
request: Request,
) -> User:
if not token:
token = request.session.get("token")

if token:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
token_data = TokenPayload(**payload)
except jwt.PyJWTError:
if request.session.get("token"):
del request.session["token"]
raise HTTPException(
status_code=401, detail="Could not validate credentials"
)

user = crud.user.get(db, id=token_data.sub)
if not user:
raise HTTPException(status_code=401, detail="User not found")

return user
else:
raise HTTPException(status_code=401, detail="Could not validate credentials")
51 changes: 51 additions & 0 deletions app/api/deps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from typing import Generator, Any, Callable
from fastapi import APIRouter as FastAPIRouter
from fastapi.types import DecoratedCallable

import redis

from app.core.config import settings
from app.database.session import SessionLocal


def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()


def get_redis_client() -> Generator:
try:
client = redis.from_url(str(settings.REDIS_URL))
yield client
finally:
client.close()


class APIRouter(FastAPIRouter):
"""
Custom APIRouter that allows for trailing slashes on endpoints.
"""

def api_route(
self, path: str, *, include_in_schema: bool = True, **kwargs: Any
) -> Callable[[DecoratedCallable], DecoratedCallable]:
if path.endswith("/"):
path = path[:-1]

add_path = super().api_route(
path, include_in_schema=include_in_schema, **kwargs
)

alternate_path = path + "/"
add_alternate_path = super().api_route(
alternate_path, include_in_schema=False, **kwargs
)

def decorator(func: DecoratedCallable) -> DecoratedCallable:
add_alternate_path(func)
return add_path(func)

return decorator
Empty file added app/api/v1/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions app/api/v1/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import APIRouter

from app.api.v1.endpoints import configuration, file, oauth, server

api_router = APIRouter()
api_router.include_router(
configuration.router, prefix="/configurations", tags=["configuration"]
)
api_router.include_router(oauth.router, prefix="/oauth", tags=["oauth"])
api_router.include_router(server.router, prefix="/servers", tags=["server"])
api_router.include_router(file.router, prefix="/files", tags=["file"])
Empty file.
Loading