diff --git a/.copier-answers.yml b/.copier-answers.yml index 211ee48..bc3a1eb 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,5 +1,5 @@ # Do NOT update manually; changes here will be overwritten by Copier -_commit: 3d28b2d +_commit: a1abb2b _src_path: https://github.com/openg2p/openg2p-fastapi-template github_ci_docker_build: true github_ci_openapi_publish: true diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index af9605c..1b0edee 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -9,18 +9,13 @@ jobs: name: Docker Build and Push runs-on: ubuntu-latest env: - NAMESPACE: ${{ secrets.docker_hub_organisation }} + NAMESPACE: ${{ secrets.docker_hub_organisation | 'openg2p' }} SERVICE_NAME: openg2p-selfservice-api steps: - uses: actions/checkout@v3 - - name: Docker build and push + - name: Docker build run: | BRANCH_NAME=$(echo ${{ github.ref }} | sed -e 's,.*/\(.*\),\1,') - echo "${{ secrets.docker_hub_token }}" | docker login -u ${{ secrets.docker_hub_actor }} --password-stdin - if [ $? -ne 0 ];then - echo "::error::Failed to Login to dockerhub" - exit 1; - fi IMAGE_ID=$NAMESPACE/$SERVICE_NAME @@ -32,9 +27,18 @@ jobs: fi echo IMAGE_ID=$IMAGE_ID echo VERSION=$VERSION + echo IMAGE_ID=$IMAGE_ID >> $GITHUB_ENV + echo VERSION=$VERSION >> $GITHUB_ENV docker build . \ --file Dockerfile \ --tag $IMAGE_ID:$VERSION - - docker push $IMAGE_ID:$VERSION + if [[ '${{ secrets.docker_hub_token }}' != '' && '${{ secrets.docker_hub_actor }}' != '' ]]; then + export DOCKER_PUSH="true" + echo DOCKER_PUSH=$DOCKER_PUSH >> $GITHUB_ENV + fi + - name: Docker push + if: env.DOCKER_PUSH == 'true' + run: | + echo "${{ secrets.docker_hub_token }}" | docker login -u ${{ secrets.docker_hub_actor }} --password-stdin + docker push ${{ env.IMAGE_ID }}:${{ env.VERSION }} diff --git a/.github/workflows/openapi-push.yml b/.github/workflows/openapi-push.yml index 66d2cae..0cefd09 100644 --- a/.github/workflows/openapi-push.yml +++ b/.github/workflows/openapi-push.yml @@ -19,16 +19,19 @@ jobs: python-version: "3.10" - name: Install app run: | - python -m pip install git+https://github.com/openg2p/openg2p-fastapi-common.git@develop\#egg=openg2p-fastapi-common\&subdirectory=openg2p-fastapi-common - python -m pip install git+https://github.com/openg2p/openg2p-fastapi-common.git@develop\#egg=openg2p-fastapi-auth\&subdirectory=openg2p-fastapi-auth + python -m pip install git+https://github.com/openg2p/openg2p-fastapi-common@develop\#subdirectory=openg2p-fastapi-common + python -m pip install git+https://github.com/openg2p/openg2p-fastapi-common@develop\#subdirectory=openg2p-fastapi-auth python -m pip install . - name: Generate openapi json run: | mkdir -p api-docs/generated python3 main.py getOpenAPI api-docs/generated/openapi.json if ! [ -z "$(git status --porcelain=v1 2>/dev/null -- api-docs/generated/openapi.json)" ]; then - export OPENAPI_CHANGED="true" - echo OPENAPI_CHANGED=$OPENAPI_CHANGED >> $GITHUB_ENV + shopt -s nocasematch + if [[ ${{ github.repository_owner }} == 'OpenG2P' ]]; then + export OPENAPI_CHANGED="true" + echo OPENAPI_CHANGED=$OPENAPI_CHANGED >> $GITHUB_ENV + fi fi - name: Commit Changes uses: EndBug/add-and-commit@v7 @@ -38,10 +41,10 @@ jobs: message: "Generated new openapi.json on push to ${{ github.event.inputs.git-ref }}" add: "api-docs/generated/openapi.json" - name: Setup nodejs - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 if: env.OPENAPI_CHANGED == 'true' with: - node-version: '14' + node-version: '18' - name: Publish to stoplight if: env.OPENAPI_CHANGED == 'true' run: | diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index bd19d03..affa8a8 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -13,4 +13,4 @@ jobs: - uses: actions/setup-python@v3 - uses: pre-commit/action@v3.0.0 with: - extra_args: --all-files + extra_args: --all-files --show-diff-on-failure diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 859667d..cdb4ae8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,8 +2,7 @@ name: Publish to PyPI on: release: - types: - - created + types: [published] jobs: publish-to-pypi: @@ -13,6 +12,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: "3.10" + - uses: actions/checkout@v3 - name: Install build dependencies run: pip install build - name: Build distribution diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3df435b..75a706c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,11 +37,14 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.py }} - - name: Install tox - run: python -m pip install tox-gh>=1.2 - - name: Setup test suite - run: tox -vv --notest + - name: Install test requirements + run: | + python -m pip install -r test-requirements.txt + python -m pip install -e . - name: Run test suite - run: tox --skip-pkg-install + run: | + pytest --cov-branch --cov-report=term-missing --cov=openg2p_portal_api --cov=tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.ruff.toml b/.ruff.toml index 79f53f2..aa1fc5b 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -11,7 +11,6 @@ ignore = [ "B008", # do not perform function calls in argument defaults "C901", # too complex ] -line-length = 110 [per-file-ignores] "__init__.py" = ["F401"] diff --git a/Dockerfile b/Dockerfile index e1aa923..30eefa0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,8 @@ ADD --chown=${container_user}:${container_user_group} main.py /app RUN python3 -m venv venv \ && . ./venv/bin/activate RUN python3 -m pip install \ - git+https://github.com/openg2p/openg2p-fastapi-common.git@develop\#egg=openg2p-fastapi-common\&subdirectory=openg2p-fastapi-common \ - git+https://github.com/openg2p/openg2p-fastapi-common.git@develop\#egg=openg2p-fastapi-auth\&subdirectory=openg2p-fastapi-auth \ + git+https://github.com/openg2p/openg2p-fastapi-common@develop\#subdirectory=openg2p-fastapi-common \ + git+https://github.com/openg2p/openg2p-fastapi-common@develop\#subdirectory=openg2p-fastapi-auth \ ./src CMD python3 main.py migrate; \ diff --git a/src/openg2p_portal_api/app.py b/src/openg2p_portal_api/app.py index 00aed0f..61385c6 100644 --- a/src/openg2p_portal_api/app.py +++ b/src/openg2p_portal_api/app.py @@ -1,4 +1,5 @@ # ruff: noqa: E402 + import asyncio from .config import Settings diff --git a/src/openg2p_portal_api/controllers/auth_controller.py b/src/openg2p_portal_api/controllers/auth_controller.py index 45c7509..74e542f 100644 --- a/src/openg2p_portal_api/controllers/auth_controller.py +++ b/src/openg2p_portal_api/controllers/auth_controller.py @@ -34,6 +34,12 @@ def __init__(self, **kwargs): super().__init__(**kwargs) self._partner_service = PartnerService.get_component() + self.router.add_api_route( + "/profile", + self.get_profile, + responses={200: {"model": Profile}}, + methods=["GET"], + ) self.router.add_api_route( "/profile", self.update_profile, diff --git a/src/openg2p_portal_api/models/orm/auth_oauth_provider.py b/src/openg2p_portal_api/models/orm/auth_oauth_provider.py index d1e73de..bf1cdba 100644 --- a/src/openg2p_portal_api/models/orm/auth_oauth_provider.py +++ b/src/openg2p_portal_api/models/orm/auth_oauth_provider.py @@ -88,7 +88,7 @@ async def get_auth_provider_from_iss(cls, iss: str) -> "AuthOauthProviderORM": select(cls) .where( and_( - cls.g2p_self_service_allowed == True, + cls.g2p_self_service_allowed == True, # noqa: E712 cls.token_endpoint.ilike(f"%{iss}%"), ) ) diff --git a/src/openg2p_portal_api/models/orm/entitlement_orm.py b/src/openg2p_portal_api/models/orm/entitlement_orm.py index d669458..8f4c0fe 100644 --- a/src/openg2p_portal_api/models/orm/entitlement_orm.py +++ b/src/openg2p_portal_api/models/orm/entitlement_orm.py @@ -1,4 +1,5 @@ from datetime import datetime + from openg2p_fastapi_common.models import BaseORMModel from sqlalchemy.orm import Mapped, mapped_column @@ -12,4 +13,4 @@ class EntitlementORM(BaseORMModel): state: Mapped[str] = mapped_column() initial_amount: Mapped[int] = mapped_column() ern: Mapped[int] = mapped_column() - date_approved: Mapped[datetime]=mapped_column() + date_approved: Mapped[datetime] = mapped_column() diff --git a/src/openg2p_portal_api/models/orm/program_orm.py b/src/openg2p_portal_api/models/orm/program_orm.py index 23f90e3..861d970 100644 --- a/src/openg2p_portal_api/models/orm/program_orm.py +++ b/src/openg2p_portal_api/models/orm/program_orm.py @@ -2,7 +2,7 @@ from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.models import BaseORMModelWithId -from sqlalchemy import ForeignKey, String, and_, func, select, or_ +from sqlalchemy import ForeignKey, String, and_, func, or_, select from sqlalchemy.ext.asyncio import async_sessionmaker from sqlalchemy.orm import Mapped, mapped_column, relationship, selectinload @@ -20,6 +20,7 @@ class ProgramORM(BaseORMModelWithId): name: Mapped[str] = mapped_column(String()) description: Mapped[str] = mapped_column(String()) + state: Mapped[str] = mapped_column(String()) is_multiple_form_submission: Mapped[str] = mapped_column() membership: Mapped[Optional[List["ProgramMembershipORM"]]] = relationship( @@ -38,7 +39,10 @@ async def get_all_programs(cls) -> List["ProgramORM"]: async_session_maker = async_sessionmaker(dbengine.get()) async with async_session_maker() as session: stmt = ( - select(cls).options(selectinload(cls.membership)).order_by(cls.id.asc()) + select(cls) + .filter(cls.state != "inactive", cls.state != "ended") + .options(selectinload(cls.membership)) + .order_by(cls.id.asc()) ) result = await session.execute(stmt) response = list(result.scalars()) @@ -64,12 +68,14 @@ async def get_all_by_program_id(cls, programid: int): async def get_all_program_by_keyword(cls, keyword: str): response = [] async_session_maker = async_sessionmaker(dbengine.get()) - async with async_session_maker() as session: + async with async_session_maker() as session: # Create a case sensitive match condition - case_sensitive_match = cls.name.like(f"%{keyword}%") & cls.name.ilike(f"%{keyword}%") + case_sensitive_match = cls.name.like(f"%{keyword}%") & cls.name.ilike( + f"%{keyword}%" + ) # Create a case insensitive match condition case_insensitive_match = cls.name.ilike(f"%{keyword}%") - + # First, select entries that match the keyword with the same case stmt = ( select(cls) @@ -225,16 +231,17 @@ async def get_benefit_details(cls, partner_id: int) -> List["ProgramORM"]: PaymentORM.status == "paid", ), ) - .where( - and_( - ProgramMembershipORM.partner_id == partner_id, - or_( - EntitlementORM.ern.isnot(None), - EntitlementORM.initial_amount != 0, - PaymentORM.amount_paid != 0, - ), + .where( + and_( + ProgramMembershipORM.partner_id == partner_id, + or_( + EntitlementORM.ern.isnot(None), + EntitlementORM.initial_amount != 0, + PaymentORM.amount_paid != 0, + ), + ) ) - ) .order_by(EntitlementORM.date_approved.desc()) + .order_by(EntitlementORM.date_approved.desc()) ) result = await session.execute(stmt) return result.all() diff --git a/src/openg2p_portal_api/services/form_service.py b/src/openg2p_portal_api/services/form_service.py index 76a3db6..97261e0 100644 --- a/src/openg2p_portal_api/services/form_service.py +++ b/src/openg2p_portal_api/services/form_service.py @@ -1,5 +1,6 @@ -from datetime import datetime import random +from datetime import datetime + from openg2p_fastapi_common.context import dbengine from openg2p_fastapi_common.service import BaseService from sqlalchemy.exc import IntegrityError @@ -123,11 +124,13 @@ async def submit_application_form( except IntegrityError: return "Error: Duplicate entry or integrity violation" - return "Successfully applied into the program!!" + return ( + f"Successfully applied into the program! Application ID: {application_id}" + ) def _compute_application_id(self): d = datetime.today().strftime("%d") m = datetime.today().strftime("%m") y = datetime.today().strftime("%y") random_number = str(random.randint(1, 100000)) - return d + m + y + random_number.zfill(5) \ No newline at end of file + return d + m + y + random_number.zfill(5) diff --git a/src/openg2p_portal_api/services/partner_service.py b/src/openg2p_portal_api/services/partner_service.py index 7b33fee..3a1bbf0 100644 --- a/src/openg2p_portal_api/services/partner_service.py +++ b/src/openg2p_portal_api/services/partner_service.py @@ -45,7 +45,7 @@ async def check_and_create_partner( "is_registrant": True, "is_group": False, "active": True, - "company_id":1, + "company_id": 1, } partner_dict["name"] = self.create_partner_process_name( partner_dict["family_name"], diff --git a/src/openg2p_portal_api/services/program_service.py b/src/openg2p_portal_api/services/program_service.py index 6f1e77d..d03e55c 100644 --- a/src/openg2p_portal_api/services/program_service.py +++ b/src/openg2p_portal_api/services/program_service.py @@ -90,7 +90,7 @@ async def get_program_by_id_service(self, programid: int, partnerid: int): return Program(**response_dict) else: - return {} + return {"message": f"Program with ID {programid} not found."} async def get_program_by_key_service(self, keyword: str): program_list = [] diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..4f53afa --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,3 @@ +pytest-cov +git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-common +git+https://github.com/openg2p/openg2p-fastapi-common@develop#subdirectory=openg2p-fastapi-auth diff --git a/tests/base_setup.py b/tests/base_setup.py deleted file mode 100644 index 3cc557b..0000000 --- a/tests/base_setup.py +++ /dev/null @@ -1,16 +0,0 @@ -from openg2p_portal_api.config import Settings - -_config = Settings.get_config() - - -def base_setup(): - _config.auth_default_issuers = ["http://localhost"] - _config.auth_default_jwks_urls = ["http://localhost/.well-known/jwks.json"] - _config.auth_id_types_ids = {"http://localhost": 3} - _config.db_dbname = "testdb" - - # Initializer() - - # initialise controller, database, programsummary, get a dummy jwt - - # ProgramController() diff --git a/tests/test_program_service.py b/tests/test_program_service.py index 3b80beb..5a2f32b 100644 --- a/tests/test_program_service.py +++ b/tests/test_program_service.py @@ -1,17 +1,5 @@ -from fastapi.testclient import TestClient -from openg2p_fastapi_common.context import app_registry +from openg2p_portal_api.app import Initializer -# from .config import Settings -# _config = Settings.get_config() -from .base_setup import base_setup -base_setup() - -client = TestClient(app_registry.get()) - - -def test_read_main(): - response = client.get("/programsummary") - - assert response.status_code == 200 - assert response.json() == {"msg": "Hello World"} +def test_program_service(): + Initializer() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 5d3f010..0000000 --- a/tox.ini +++ /dev/null @@ -1,20 +0,0 @@ -[tox] -env_list = - clean, - py39, - py310 -minversion = 4.11.3 - -[testenv] -description = run the tests with pytest -commands = pytest --cov=. -deps = - git+https://github.com/openg2p/openg2p-fastapi-common.git@develop\#egg=openg2p-fastapi-common&subdirectory=openg2p-fastapi-common - git+https://github.com/openg2p/openg2p-fastapi-common.git@develop\#egg=openg2p-fastapi-auth&subdirectory=openg2p-fastapi-auth - ./ - pytest-cov - -[testenv:clean] -deps = coverage -skip_install = true -commands = coverage erase