Skip to content

Commit

Permalink
Merge branch 'develop' into edit-manufacturer-#65
Browse files Browse the repository at this point in the history
  • Loading branch information
MatteoGuarnaccia5 committed Oct 23, 2023
2 parents 41c2f9c + b38a255 commit f23cbe8
Show file tree
Hide file tree
Showing 31 changed files with 3,413 additions and 256 deletions.
47 changes: 40 additions & 7 deletions .github/workflows/.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0

- name: Set up Python
uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: "3.10"
cache: "pip"
Expand All @@ -36,10 +36,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0

- name: Set up Python
uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: "3.10"
cache: "pip"
Expand All @@ -63,18 +63,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0

- name: Start MongoDB
uses: supercharge/mongodb-github-action@d26215f71b2ce60420a2a3776a25893d11a65f85 #v1.9.0
uses: supercharge/mongodb-github-action@b0a1493307c4e9b82ed61f3858d606c5ff190c64 #v1.10.0
with:
mongodb-version: "6.0"
mongodb-username: root
mongodb-password: example
mongodb-db: test-ims

- name: Set up Python
uses: actions/setup-python@bd6b4b6205c4dbad673328db7b31b7fab9e241c0 # v4.6.1
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: "3.10"
cache: "pip"
Expand All @@ -91,3 +91,36 @@ jobs:

- name: Run e2e tests
run: DATABASE__NAME="test-ims" pytest test/e2e/ --cov

docker:
# This job triggers only if all the other jobs succeed and does different things depending on the context.
# The job builds the Docker image in all cases and also pushes the image to Harbor only if something is
# pushed to the develop branch.
needs: [linting, unit-tests, e2e-tests]
name: Docker
runs-on: ubuntu-latest
steps:
- name: Check out repo
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0

- name: Login to Harbor
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: harbor.stfc.ac.uk/inventory-management-system
username: ${{ secrets.HARBOR_USERNAME }}
password: ${{ secrets.HARBOR_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: harbor.stfc.ac.uk/inventory-management-system/ims-api

- name: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' && 'Build and push Docker image to Harbor' || 'Build Docker image' }}
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
file: ./Dockerfile.prod
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/develop' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
.vscode/
mongodb/data/*
!mongodb/data/.keep
logging.ini
inventory_management_system_api/logging.ini


# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
38 changes: 38 additions & 0 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM python:3.10-alpine3.18

WORKDIR /inventory-management-system-api-run

COPY README.md pyproject.toml ./
# Copy inventory_management_system_api source files
COPY inventory_management_system_api/ inventory_management_system_api/

RUN set -eux; \
\
# Install pip dependencies \
python -m pip install --no-cache-dir .; \
\
# Create loging.ini from its .example file \
cp inventory_management_system_api/logging.example.ini inventory_management_system_api/logging.ini; \
\
# Create a non-root user to run as \
addgroup -S inventory-management-system-api; \
adduser -S -D -G inventory-management-system-api -H -h /inventory-management-system-api-run inventory-management-system-api; \
\
# Create a log file \
touch inventory-management-system-api.log; \
# Change ownership of log file - app will need to write to it
chown -R inventory-management-system-api:inventory-management-system-api inventory-management-system-api.log;

USER inventory-management-system-api

ENV API__TITLE="Inventory Management System API"
ENV API__DESCRIPTION="This is the API for the Inventory Management System"
ENV DATABASE__PROTOCOL=mongodb
ENV DATABASE__USERNAME=root
ENV DATABASE__PASSWORD=example
ENV DATABASE__HOSTNAME=localhost
ENV DATABASE__PORT=27017
ENV DATABASE__NAME=ims

CMD ["uvicorn", "inventory_management_system_api.main:app", "--host", "0.0.0.0", "--port", "8000"]
EXPOSE 8000
65 changes: 40 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,80 @@

## How to Run

This is a Python microservice created using FastAPI and requires a MongoDB instance to run against. If you are using
Docker to run the application, the `docker-compose.yml file` has already been configured to start a MongoDB instance
that can be accessed at `localhost:27017` using `root` as the username and `example` as the password.
This is a Python microservice created using FastAPI and requires a MongoDB instance to run against.

### Prerequisites
- Docker installed (if you want to run the microservice inside Docker)
- Python 3.10 (or above) and MongoDB 6.0 installed on your machine if you are not using Docker
- [MongoDB Compass](https://www.mongodb.com/products/compass) installed (if you want to interact with the database using a GUI)
- This repository cloned

### Docker Setup
The easiest way to run the application with Docker for local development is using the `docker-compose.yml` file. It is
configured to spin up a MongoDB instance that can be accessed at `localhost:27017` using `root` as the username and
`example` as the password. It also starts the application in a reload mode using the `Dockerfile`. Use the `Dockerfile`
or `Dockerfile.prod` to run just the application itself in a container. The former is for local development and must
not be used in production.

1. Ensure that Docker is installed and running on your machine.
2. Clone the repository and navigate to the project directory:
```bash
git clone [email protected]:ral-facilities/inventory-management-system-api.git
cd inventory-management-system-api
3. Create a `logging.ini` file.
Ensure that Docker is installed and running on your machine before proceeding.

#### Using Docker Compose File
1. Create a `logging.ini` file from the example in the root of the project directory:
```bash
cp logging.example.ini logging.ini
```

4. Build and start the Docker containers:
2. Build and start the Docker containers:
```bash
docker-compose up
```
The microservice should now be running inside Docker at http://localhost:8000. The Swagger UI can be accessed
at http://localhost:8000/docs.
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs. A MongoDB instance should also be running at http://localhost:27017.

### Local Setup
#### Using Dockerfiles

1. Build an image using either the `Dockerfile` or `Dockerfile.prod` from the root of the project directory:
```bash
docker build -f Dockerfile.prod -t ims_api_image .
```

1. Clone the repository and navigate to the project directory:
2. Start the container using the image built and map it to port `8000` locally:
```bash
git clone [email protected]:ral-facilities/inventory-management-system-api.git
cd inventory-management-system-api
docker run -p 8000:8000 --name ims_api_container ims_api_image
```
2. Create a Python virtual environment and activate it:
or with values for the environment variables:
```bash
docker run -p 8000:8000 --name ims_api_container --env DATABASE__NAME=test-ims ims_api_image
```
The microservice should now be running inside Docker at http://localhost:8000 and its Swagger UI could be accessed
at http://localhost:8000/docs.

### Local Setup

Ensure that MongoDB is installed and running on your machine before proceeding.

1. Create a Python virtual environment and activate it in the root of the project directory:
```bash
python -m venv venv
source venv/bin/activate
```
3. Install the required dependencies using pip:
2. Install the required dependencies using pip:
```bash
pip install .[dev]
```
4. Create a `.env` file using the `.env.example` file and modify the values inside accordingly.
5. Create a `logging.ini` file using the `logging.example.ini` file and modify it accordingly.
6. Ensure that MongoDB is running locally. If it's not installed, you can follow the official MongoDB installation guide
for your operating system.
7. Start the microservice using Uvicorn from the project directory:
3. Create a `.env` file using the `.env.example` file and modify the values inside accordingly.
4. Create a `logging.ini` file using the `logging.example.ini` file and modify it accordingly.
5. Start the microservice using Uvicorn:
```bash
uvicorn inventory_management_system_api.main:app --log-config inventory_management_system_api/logging.ini --reload
```
The microservice should now be running locally at http://localhost:8000. The Swagger UI can be accessed
at http://localhost:8000/docs.
8. To run the unit tests, run :
6. To run the unit tests, run :
```bash
pytest test/unit/
```
9. To run the e2e tests, ensure that MongoDB is running locally and run:
7. To run the e2e tests, run:
```bash
DATABASE__NAME="test-ims" pytest test/e2e/
```
4 changes: 2 additions & 2 deletions inventory_management_system_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@

from inventory_management_system_api.core.config import config
from inventory_management_system_api.core.logger_setup import setup_logger
from inventory_management_system_api.routers.v1 import catalogue_category, manufacturer
from inventory_management_system_api.routers.v1 import catalogue_item
from inventory_management_system_api.routers.v1 import catalogue_category, catalogue_item, system, manufacturer

app = FastAPI(title=config.api.title, description=config.api.description)

Expand Down Expand Up @@ -66,6 +65,7 @@ async def custom_validation_exception_handler(request: Request, exc: RequestVali
app.include_router(catalogue_category.router)
app.include_router(catalogue_item.router)
app.include_router(manufacturer.router)
app.include_router(system.router)


@app.get("/")
Expand Down
43 changes: 43 additions & 0 deletions inventory_management_system_api/models/system.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""
Module for defining the database models for representing a System
"""

from typing import Optional

from pydantic import BaseModel, Field

from inventory_management_system_api.models.custom_object_id_data_types import CustomObjectIdField, StringObjectIdField


class SystemIn(BaseModel):
"""
Input database model for a System
"""

name: str
location: str
owner: str
importance: str
description: str

# Used for uniqueness checks (sanitised name)
code: str
# These two are purely for front end navigation
path: str
parent_path: str

parent_id: Optional[CustomObjectIdField] = None


class SystemOut(SystemIn):
"""
Output database model for a System
"""

id: StringObjectIdField = Field(alias="_id")
parent_id: Optional[StringObjectIdField] = None

# Required just for unit tests
class Config:
# pylint: disable=C0115
allow_population_by_field_name = True
29 changes: 9 additions & 20 deletions inventory_management_system_api/repositories/catalogue_category.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Module for providing a repository for managing catalogue categories in a MongoDB database.
"""
import logging
from typing import Optional, List
from typing import List, Optional

from fastapi import Depends
from pymongo.collection import Collection
Expand All @@ -11,11 +11,12 @@
from inventory_management_system_api.core.custom_object_id import CustomObjectId
from inventory_management_system_api.core.database import get_database
from inventory_management_system_api.core.exceptions import (
MissingRecordError,
DuplicateRecordError,
ChildrenElementsExistError,
DuplicateRecordError,
MissingRecordError,
)
from inventory_management_system_api.models.catalogue_category import CatalogueCategoryIn, CatalogueCategoryOut
from inventory_management_system_api.repositories import utils

logger = logging.getLogger()

Expand Down Expand Up @@ -72,7 +73,7 @@ def delete(self, catalogue_category_id: str) -> None:
:raises MissingRecordError: If the catalogue category doesn't exist.
"""
catalogue_category_id = CustomObjectId(catalogue_category_id)
if self._has_children_elements(str(catalogue_category_id)):
if self._has_child_elements(catalogue_category_id):
raise ChildrenElementsExistError(
f"Catalogue category with ID {str(catalogue_category_id)} has children elements and cannot be deleted"
)
Expand All @@ -96,7 +97,7 @@ def get(self, catalogue_category_id: str) -> Optional[CatalogueCategoryOut]:
return CatalogueCategoryOut(**catalogue_category)
return None

def update(self, catalogue_category_id: str, catalogue_category: CatalogueCategoryIn):
def update(self, catalogue_category_id: str, catalogue_category: CatalogueCategoryIn) -> CatalogueCategoryOut:
"""
Update a catalogue category by its ID in a MongoDB database.
Expand All @@ -113,7 +114,7 @@ def update(self, catalogue_category_id: str, catalogue_category: CatalogueCatego
:raises DuplicateRecordError: If a duplicate catalogue category is found within the parent catalogue category.
"""
catalogue_category_id = CustomObjectId(catalogue_category_id)
if self._has_children_elements(str(catalogue_category_id)):
if self._has_child_elements(catalogue_category_id):
raise ChildrenElementsExistError(
f"Catalogue category with ID {str(catalogue_category_id)} has children elements and cannot be updated"
)
Expand Down Expand Up @@ -145,18 +146,7 @@ def list(self, path: Optional[str], parent_path: Optional[str]) -> List[Catalogu
:return: A list of catalogue categories, or an empty list if no catalogue categories are returned by the
database.
"""
query = {}
if path:
query["path"] = path
if parent_path:
query["parent_path"] = parent_path

message = "Retrieving all catalogue categories from the database"
if not query:
logger.info(message)
else:
logger.info("%s matching the provided filter(s)", message)
logger.debug("Provided filter(s): %s", query)
query = utils.path_query(path, parent_path, "catalogue categories")

catalogue_categories = self._catalogue_categories_collection.find(query)
return [CatalogueCategoryOut(**catalogue_category) for catalogue_category in catalogue_categories]
Expand All @@ -176,7 +166,7 @@ def _is_duplicate_catalogue_category(self, parent_id: Optional[str], code: str)
count = self._catalogue_categories_collection.count_documents({"parent_id": parent_id, "code": code})
return count > 0

def _has_children_elements(self, catalogue_category_id: str) -> bool:
def _has_child_elements(self, catalogue_category_id: CustomObjectId) -> bool:
"""
Check if a catalogue category has children elements based on its ID.
Expand All @@ -186,7 +176,6 @@ def _has_children_elements(self, catalogue_category_id: str) -> bool:
:param catalogue_category_id: The ID of the catalogue category to check.
:return: True if the catalogue category has children elements, False otherwise.
"""
catalogue_category_id = CustomObjectId(catalogue_category_id)
logger.info("Checking if catalogue category with ID '%s' has children elements", catalogue_category_id)
# Check if it has catalogue categories
query = {"parent_id": catalogue_category_id}
Expand Down
Loading

0 comments on commit f23cbe8

Please sign in to comment.