From b29c196b56c73e32d6c6456e6de717ee155bad89 Mon Sep 17 00:00:00 2001 From: Kreesh Modi Date: Tue, 3 Mar 2026 17:00:45 +0530 Subject: [PATCH 01/83] Add hello API endpoint --- backend/python/django_app/urls.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index 0418448be..a06b7ff64 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -1,11 +1,23 @@ +# django_app/urls.py + from django.contrib import admin from django.urls import path -from django.http import HttpResponse +from django.http import JsonResponse + + +def hello_name(request): + """ + A simple view that returns 'Hello, {name}' in JSON format. + Uses a query parameter named 'name'. + """ + # Get 'name' from the query string, default to 'World' if missing + name = request.GET.get("name", "World") + return JsonResponse({"message": f"Hello, {name}!"}) -def hello_world(request): - return HttpResponse("Hello, world! This is our interneers-lab Django server.") urlpatterns = [ path('admin/', admin.site.urls), - path('hello/', hello_world), + path('hello/', hello_name), + # Example usage: /hello/?name=Bob + # returns {"message": "Hello, Bob!"} ] From 6481bc1d83646df2acf7e97295f9384d796a67e5 Mon Sep 17 00:00:00 2001 From: Kreesh Modi Date: Wed, 4 Mar 2026 23:44:35 +0530 Subject: [PATCH 02/83] GET API using hexagonal architecture --- backend/python/README.md | 46 +++++++++++++++---- .../python/django_app/adapters/api/urls.py | 6 +++ .../python/django_app/adapters/api/views.py | 9 ++++ .../django_app/application/greeter_service.py | 8 ++++ backend/python/django_app/domain/greeting.py | 10 ++++ backend/python/django_app/ports/greeter.py | 4 ++ backend/python/django_app/urls.py | 3 +- 7 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 backend/python/django_app/adapters/api/urls.py create mode 100644 backend/python/django_app/adapters/api/views.py create mode 100644 backend/python/django_app/application/greeter_service.py create mode 100644 backend/python/django_app/domain/greeting.py create mode 100644 backend/python/django_app/ports/greeter.py diff --git a/backend/python/README.md b/backend/python/README.md index 1522f5e23..8b74f0811 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -1,13 +1,34 @@ +# WEEK 1 + +### What I built + +- A **GET endpoint** that returns a greeting message using a query parameter: + - `GET /hello-world/` → `{"message": "Hello, World!"}` + - `GET /hello-world/?name=Kreesh` → `{"message": "Hello, Kreesh!"}` +- Tested the endpoint using **Postman**. +- Verified changes locally and pushed updates to the repository. + +### Hexagonal architecture overview + +The implementation is organized into clear layers to keep business logic independent of Django: + +- **domain/**: Pure business logic (e.g., formatting/validating the greeting). No Django imports. +- **application/**: Use-case layer that coordinates the feature (calls domain and returns a result). +- **ports/**: Contract boundary for the use-case (defines what the core exposes). +- **adapters/api/**: Django HTTP layer (views + urls) that translates HTTP requests into application calls and returns JSON. + +--- + # Interneers Lab - Backend in Python Welcome to the **Interneers Lab 2026** Python backend! This serves as a minimal starter kit for learning and experimenting with: + - **Django** (Python) - **MongoDB** (via Docker Compose) - Development environment in **VSCode** (recommended) **Important:** Use the **same email** you shared during onboarding when configuring Git and related tools. That ensures consistency across all internal systems. - --- ## Table of Contents @@ -44,6 +65,7 @@ These are the essential tools you need: Homebrew is a popular package manager for macOS, making it easy to install and update software (like Python, Docker, etc.). **Install**: + ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` @@ -55,7 +77,6 @@ These are the essential tools you need: This is the recommended version for the module's Python-related tasks, ensuring consistency across projects. **Install or Upgrade**: - - macOS (with Homebrew): `brew install python` or use [pyenv](https://github.com/pyenv/pyenv): ```bash brew install pyenv @@ -70,6 +91,7 @@ These are the essential tools you need: ```bash python3 --version ``` + You should see something like `Python 3.14.x`. If you are getting an older version, you can either: @@ -92,8 +114,8 @@ These are the essential tools you need: - or use `python3 -m venv venv` **Verify** - - Try to activate the venv using the following command: + ```bash source venv/bin/activate # macOS/Linux .\venv\Scripts\activate # Windows @@ -102,11 +124,12 @@ These are the essential tools you need: - In most machines, your terminal prompt will be prefixed with something like `(venv)`. Check which Python is being used: - - macOS/Linux: + ```bash which python ``` + This should return a path inside the `venv/` directory (e.g., `.../backend/python/venv/bin/python`) - Windows: @@ -115,7 +138,6 @@ These are the essential tools you need: ``` This should return a path inside `venv\Scripts\python.exe`. - 4. **Docker** & **Docker Compose** **Why?** @@ -123,7 +145,6 @@ These are the essential tools you need: We use Docker to run MongoDB (and potentially other services) in containers, preventing "works on my machine" issues. **Install** - - [Docker Desktop for Mac](https://www.docker.com/products/docker-desktop/) - [Docker Desktop for Windows](https://www.docker.com/products/docker-desktop/) @@ -131,7 +152,6 @@ These are the essential tools you need: Verify version and successful installation with `docker --version` and `docker compose version`. - 5. **API & MongoDB Tools** - **[Postman](https://www.postman.com/downloads/)**, **[Insomnia](https://insomnia.rest/download)**, or **[Paw](https://paw.cloud/client) (only for mac)** for API testing - **[MongoDB Compass](https://www.mongodb.com/try/download/compass)** or a **[VSCode MongoDB](https://code.visualstudio.com/docs/azure/mongodb)** extension @@ -155,6 +175,7 @@ To activate the virtual environment: # macOS/Linux source venv/bin/activate ``` + ```Powershell # on Windows Powershell: Set-ExecutionPolicy RemoteSigned -Scope CurrentUser @@ -169,6 +190,7 @@ pip3 install -r requirements.txt ``` By default, **requirements.txt** includes: + - **Django** 6.0.2 - **pymongo** 4.16.0 (MongoDB driver) @@ -241,7 +263,7 @@ Confirm that all meet the minimum version requirements. - **Docker** Allows you to visualize, manage, and interact with Docker containers and images directly in VSCode. -- *(Optional)* **MongoDB for VSCode** +- _(Optional)_ **MongoDB for VSCode** Lets you connect to and browse your MongoDB databases, run queries, and view results without leaving VSCode. --- @@ -303,17 +325,20 @@ source venv/bin/activate # macOS/Linux ``` Install dependencies (if you haven't): + ```bash cd backend/python # if you are not inside backend/python already. pip3 install -r requirements.txt ``` Start the server on port 8001: + ```bash python manage.py runserver 8001 ``` You should see: + ``` Starting development server at http://127.0.0.1:8001/ ``` @@ -325,11 +350,13 @@ Install a REST client like Postman (if you haven't already). Create a new GET request. Enter the endpoint, for example: + ``` http://127.0.0.1:8001/hello/?name=Bob ``` Send the request. You should see a JSON response: + ```json { "message": "Hello, Bob!" @@ -368,6 +395,7 @@ python manage.py test ```bash docker compose ps ``` + Note: This command displays the status of the containers, including whether they are running, their assigned ports, and their names, as defined in the docker-compose.yaml file. If you have set up a MongoDB server using Docker and connected it to your Django application, you can use this command to verify that the MongoDB container is running properly. --- @@ -395,6 +423,7 @@ mongodb://root:example@localhost:27019/?authSource=admin To ensure flexibility across environments, use environment variables for the MongoDB connection. For example: #### Example `settings.py` (Django + pymongo): + ```python # Database # https://docs.djangoproject.com/en/6.0/ref/settings/#databases @@ -427,6 +456,7 @@ DATABASES = {} --- ## Important Note on `settings.py` + - You should commit `settings.py` so the Django configuration is shared. - However, never commit secrets (API keys, passwords) directly. Use environment variables or `.env` files (excluded via `.gitignore`). diff --git a/backend/python/django_app/adapters/api/urls.py b/backend/python/django_app/adapters/api/urls.py new file mode 100644 index 000000000..ae37aa5b0 --- /dev/null +++ b/backend/python/django_app/adapters/api/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from .views import hello_world + +urlpatterns = [ + path("hello-world/", hello_world, name="hello-world") +] diff --git a/backend/python/django_app/adapters/api/views.py b/backend/python/django_app/adapters/api/views.py new file mode 100644 index 000000000..9549b907b --- /dev/null +++ b/backend/python/django_app/adapters/api/views.py @@ -0,0 +1,9 @@ +from django.http import JsonResponse +from django_app.application.greeter_service import greet + + +def hello_world(request): + # url is /hello-world/?name="Kreesh" it returns Kreesh + name = request.GET.get("name") + message = greet(name) + return JsonResponse({"message": message}) diff --git a/backend/python/django_app/application/greeter_service.py b/backend/python/django_app/application/greeter_service.py new file mode 100644 index 000000000..b35ceadc0 --- /dev/null +++ b/backend/python/django_app/application/greeter_service.py @@ -0,0 +1,8 @@ +# implement port contract using domain logic +# use case, what should happen when someone calls this feature + +from django_app.domain.greeting import greeting + + +def greet(name): + return greeting(name) diff --git a/backend/python/django_app/domain/greeting.py b/backend/python/django_app/domain/greeting.py new file mode 100644 index 000000000..829cad28b --- /dev/null +++ b/backend/python/django_app/domain/greeting.py @@ -0,0 +1,10 @@ +# domain : core logic + +def greeting(name): + if name == None: + name = "" + name = name.strip() + if (name == ""): + return "Hello, World!" + else: + return "Hello, "+name+"!" diff --git a/backend/python/django_app/ports/greeter.py b/backend/python/django_app/ports/greeter.py new file mode 100644 index 000000000..046d30ff5 --- /dev/null +++ b/backend/python/django_app/ports/greeter.py @@ -0,0 +1,4 @@ +# ports : core sys communicates with outside world + +def greet(name): + return "PORT ONLY - implement in application" diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index a06b7ff64..53ef8054c 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -1,7 +1,7 @@ # django_app/urls.py from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.http import JsonResponse @@ -20,4 +20,5 @@ def hello_name(request): path('hello/', hello_name), # Example usage: /hello/?name=Bob # returns {"message": "Hello, Bob!"} + path("", include("django_app.adapters.api.urls")), ] From 3bbcdc4346b9fb7b2728b9ab93173e48919ddddc Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Thu, 5 Mar 2026 12:15:40 +0530 Subject: [PATCH 03/83] readme updated --- backend/python/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index 8b74f0811..c9085c984 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -5,8 +5,6 @@ - A **GET endpoint** that returns a greeting message using a query parameter: - `GET /hello-world/` → `{"message": "Hello, World!"}` - `GET /hello-world/?name=Kreesh` → `{"message": "Hello, Kreesh!"}` -- Tested the endpoint using **Postman**. -- Verified changes locally and pushed updates to the repository. ### Hexagonal architecture overview From 22a3450c13302233d0ec14a24cf2b9260b8e0f87 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 11 Mar 2026 20:32:58 +0530 Subject: [PATCH 04/83] week2- REST API product model created --- backend/python/django_app/settings.py | 2 + backend/python/django_app/urls.py | 1 + backend/python/week2/__init__.py | 0 backend/python/week2/admin.py | 3 + backend/python/week2/apps.py | 5 ++ backend/python/week2/migrations/__init__.py | 0 backend/python/week2/models.py | 24 ++++++++ backend/python/week2/serializers.py | 47 ++++++++++++++ backend/python/week2/store.py | 61 ++++++++++++++++++ backend/python/week2/tests.py | 3 + backend/python/week2/urls.py | 10 +++ backend/python/week2/views.py | 68 +++++++++++++++++++++ 12 files changed, 224 insertions(+) create mode 100644 backend/python/week2/__init__.py create mode 100644 backend/python/week2/admin.py create mode 100644 backend/python/week2/apps.py create mode 100644 backend/python/week2/migrations/__init__.py create mode 100644 backend/python/week2/models.py create mode 100644 backend/python/week2/serializers.py create mode 100644 backend/python/week2/store.py create mode 100644 backend/python/week2/tests.py create mode 100644 backend/python/week2/urls.py create mode 100644 backend/python/week2/views.py diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index 2d7ea95db..464f9b7ec 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -37,6 +37,8 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", + "week2", ] MIDDLEWARE = [ diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index 53ef8054c..3d67b7df8 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -21,4 +21,5 @@ def hello_name(request): # Example usage: /hello/?name=Bob # returns {"message": "Hello, Bob!"} path("", include("django_app.adapters.api.urls")), + path("week2/", include("week2.urls")), ] diff --git a/backend/python/week2/__init__.py b/backend/python/week2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week2/admin.py b/backend/python/week2/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/python/week2/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/python/week2/apps.py b/backend/python/week2/apps.py new file mode 100644 index 000000000..975b782db --- /dev/null +++ b/backend/python/week2/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class Week2Config(AppConfig): + name = "week2" diff --git a/backend/python/week2/migrations/__init__.py b/backend/python/week2/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week2/models.py b/backend/python/week2/models.py new file mode 100644 index 000000000..7cfe09405 --- /dev/null +++ b/backend/python/week2/models.py @@ -0,0 +1,24 @@ +from django.db import models +from decimal import Decimal + + +class Product: + def __init__(self, id, name, description, category, price, brand, quantity): + self.id = id + self.name = name + self.description = description + self.category = category + self.price = Decimal(str(price)) + self.brand = brand + self.quantity = quantity + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "description": self.description, + "category": self.category, + "price": str(self.price), + "brand": self.brand, + "quantity": self.quantity, + } diff --git a/backend/python/week2/serializers.py b/backend/python/week2/serializers.py new file mode 100644 index 000000000..59d363494 --- /dev/null +++ b/backend/python/week2/serializers.py @@ -0,0 +1,47 @@ +# serializer : +# 1.> validate incoming API data +# 2.> converts data into clean python format + + +from rest_framework import serializers + + +class ProductSerializer(serializers.Serializer): + # id is generated by store therefore read only therefore user should not send ID + id = serializers.IntegerField(read_only=True) + name = serializers.CharField(max_length=200) + description = serializers.CharField() + category = serializers.CharField(max_length=100) + price = serializers.DecimalField(max_digits=10, decimal_places=2) + # brand is optional even if client sends {brand = ""} then also it will work + brand = serializers.CharField( + max_length=100, required=False, allow_blank=True) + quantity = serializers.IntegerField() + + # custom validations + + # {name = { }} should be rejected + def validate_name(self, value): + if not value.strip(): + raise serializers.ValidationError("Name cannot be empty") + return value + + def validate_category(self, value): + if not value.strip(): + raise serializers.ValidationError("Category cannot be empty.") + return value + + def validate_price(self, value): + if value <= 0: + raise serializers.ValidationError("Price must be greater than 0.") + return value + + def validate_quantity(self, value): + if value < 0: + raise serializers.ValidationError("Quantity cannot be negative.") + return value + + def validate_brand(self, value): + if value != "" and not value.strip(): + raise serializers.ValidationError("Brand cannot be only spaces.") + return value diff --git a/backend/python/week2/store.py b/backend/python/week2/store.py new file mode 100644 index 000000000..30e6b03f7 --- /dev/null +++ b/backend/python/week2/store.py @@ -0,0 +1,61 @@ +# store.py : temporary in-memory database + +from .models import Product + + +class ProductStore: + def __init__(self): + self.products = {} + self.next_id = 1 + + # creating new data + + def create(self, data): + # it is python object not JSON + product = Product( + id=self.next_id, + name=data["name"], + description=data["description"], + category=data["category"], + price=data["price"], + brand=data.get("brand", ""), + quantity=data["quantity"], + ) + self.products[self.next_id] = product + self.next_id += 1 + return product + + # getting existing data + + def get(self, product_id): + return self.products.get(product_id) + + # listing all data + + def list_all(self): + return list(self.products.values()) + + # updating data + + def update(self, product_id, data): + product = self.products.get(product_id) + if not product: + return None + product.name = data["name"] + product.description = data["description"] + product.category = data["category"] + product.price = data["price"] + product.brand = data.get("brand", "") + product.quantity = data["quantity"] + return product + + # deleting data + + def delete(self, product_id): + if product_id in self.products: + del self.products[product_id] + return True + return False + + +product_store = ProductStore() diff --git a/backend/python/week2/tests.py b/backend/python/week2/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/python/week2/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/python/week2/urls.py b/backend/python/week2/urls.py new file mode 100644 index 000000000..519edc5f5 --- /dev/null +++ b/backend/python/week2/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from .views import ProductDetailAPIView, ProductListCreateAPIView + +urlpatterns = [ + path("products/", ProductListCreateAPIView.as_view(), + name="product-list-create"), + path("products//", + ProductDetailAPIView.as_view(), name="product-detail"), +] diff --git a/backend/python/week2/views.py b/backend/python/week2/views.py new file mode 100644 index 000000000..493480614 --- /dev/null +++ b/backend/python/week2/views.py @@ -0,0 +1,68 @@ +# view : code that runs when a client hits an endpoint i.e receives http request and decides what to do +# eg GET/products/, PUT/products/1 + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from .serializers import ProductSerializer +from .store import product_store + + +class ProductListCreateAPIView(APIView): + # list all product + def get(self, request): + products = product_store.list_all() + product_data = [] + for product in products: + product_data.append(product.to_dict()) + # output serialisation + # without many = true DRF would expect one product + serializer = ProductSerializer(product_data, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + # create a product + def post(self, request): + # input serialisation + serializer = ProductSerializer(data=request.data) + if serializer.is_valid(): + product = product_store.create(serializer.validated_data) + response_serializer = ProductSerializer(product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ProductDetailAPIView(APIView): + # get specific product + def get(self, request, product_id): + product = product_store.get(product_id) + if not product: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) + else: + serializer = ProductSerializer(product.to_dict()) + return Response(serializer.data, status=status.HTTP_200_OK) + + # update one product + def put(self, request, product_id): + existing_product = product_store.get(product_id) + if not existing_product: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND,) + else: + # validate new data first + serializer = ProductSerializer(data=request.data) + if (serializer.is_valid()): + updated_product = product_store.update( + product_id, serializer.validated_data) + response_serializer = ProductSerializer( + updated_product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # delete data + def delete(self, request, product_id): + deleted = product_store.delete(product_id) + if not deleted: + return Response({"error": "Product not found."}, status=status.HTTP_404_NOT_FOUND,) + else: + return Response(status=status.HTTP_204_NO_CONTENT) From f3609e420ea5acd70038fb339219af335886bfee Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 11 Mar 2026 21:15:51 +0530 Subject: [PATCH 05/83] week2- REST API product model created --- backend/python/week2/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/python/week2/views.py b/backend/python/week2/views.py index 493480614..0e3af5ba6 100644 --- a/backend/python/week2/views.py +++ b/backend/python/week2/views.py @@ -18,6 +18,7 @@ def get(self, request): # output serialisation # without many = true DRF would expect one product serializer = ProductSerializer(product_data, many=True) + # Response(product_data, status=status.HTTP_200_OK) is also valid return Response(serializer.data, status=status.HTTP_200_OK) # create a product From 3f6e12ee4e4cb3108daf713bd9da4fcdc37ee293 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 11 Mar 2026 21:24:30 +0530 Subject: [PATCH 06/83] readme updated --- backend/python/README.md | 202 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 1 deletion(-) diff --git a/backend/python/README.md b/backend/python/README.md index c9085c984..d7fdbfd5c 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -17,6 +17,206 @@ The implementation is organized into clear layers to keep business logic indepen --- +# WEEK 2 + +## What I built + +In Week 2, I built a **Product CRUD API** using **Django REST Framework (DRF)**. + +This API supports: + +- **Create** a product +- **List** all products +- **Get** one product by ID +- **Update** a product by ID +- **Delete** a product by ID + +Current endpoints: + +- `GET /week2/products/` +- `POST /week2/products/` +- `GET /week2/products//` +- `PUT /week2/products//` +- `DELETE /week2/products//` + +Important note: for Week 2, the implementation uses **in-memory storage**, so products are stored in a Python dictionary while the server is running. Data is **not persisted** to the database yet. + +## Quick theory: what is CRUD? + +CRUD stands for: + +- **Create** +- **Read** +- **Update** +- **Delete** + +In this project: + +- `POST /week2/products/` → Create +- `GET /week2/products/` → Read all +- `GET /week2/products//` → Read one +- `PUT /week2/products//` → Update +- `DELETE /week2/products//` → Delete + +--- + +## Quick theory: HTTP verbs used + +### GET + +Used to fetch data. + +Examples: + +- `GET /week2/products/` +- `GET /week2/products/1/` + +### POST + +Used to create new data. + +Example: + +- `POST /week2/products/` + +### PUT + +Used to update existing data fully. + +Example: + +- `PUT /week2/products/1/` + +### DELETE + +Used to remove data. + +Example: + +- `DELETE /week2/products/1/` + +--- + +## Week 2 architecture overview + +The Week 2 implementation is split into small layers so each file has one responsibility. + +### `models.py` + +Defines the `Product` object structure. + +This is a **plain Python class**, not a Django ORM model in this week’s version. + +Responsibilities: + +- define product fields +- hold product data +- convert object into dictionary using `to_dict()` + +### `store.py` + +Acts as a **temporary in-memory database**. + +Responsibilities: + +- store products in a dictionary +- generate product IDs +- create/get/list/update/delete products + +### `serializers.py` + +Handles validation and API data structure. + +Responsibilities: + +- validate incoming request data +- convert incoming data into cleaned Python data +- define the API shape for Product + +### `views.py` + +Handles HTTP requests and responses. + +Responsibilities: + +- receive request +- call serializer +- call store +- return DRF `Response` + +### `urls.py` + +Connects API endpoints to views. + +### `django_app/urls.py` + +Includes `week2.urls` under `/week2/`. + +--- + +## Architecture flow + +````text +Client (Browser / Postman / Frontend) + ↓ +HTTP Request + ↓ +request.data = { + "name": "Wireless Mouse", + "description": "2.4 GHz ergonomic mouse", + "category": "Electronics", + "price": "799.00", + "brand": "Logitech", + "quantity": 25 +} + ↓ +django_app/urls.py + ↓ +week2/urls.py + ↓ +views.py + ↓ +serializers.py + ↓ +serializer.validated_data = { + "name": "Wireless Mouse", + "description": "2.4 GHz ergonomic mouse", + "category": "Electronics", + "price": Decimal("799.00"), + "brand": "Logitech", + "quantity": 25 +} + ↓ +store.py + ↓ +Product( + id=1, + name="Wireless Mouse", + description="2.4 GHz ergonomic mouse", + category="Electronics", + price=Decimal("799.00"), + brand="Logitech", + quantity=25 +) + ↓ +to_dict() +↓ +{ + "id": 1, + "name": "Wireless Mouse", + "description": "2.4 GHz ergonomic mouse", + "category": "Electronics", + "price": "799.00", + "brand": "Logitech", + "quantity": 25 +} + ↓ +serializer.data / Response(...) + ↓ +JSON Response back to client + +--- + # Interneers Lab - Backend in Python Welcome to the **Interneers Lab 2026** Python backend! This serves as a minimal starter kit for learning and experimenting with: @@ -66,7 +266,7 @@ These are the essential tools you need: ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - ``` +```` 2. **Python 3.14** (3.12 or higher required) From 58b4c0aad80057ffe158bbed4983d721f5e5dff5 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 11 Mar 2026 21:26:14 +0530 Subject: [PATCH 07/83] readme updated --- backend/python/README.md | 80 ++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index d7fdbfd5c..e4819992e 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -156,63 +156,62 @@ Includes `week2.urls` under `/week2/`. ## Architecture flow -````text Client (Browser / Postman / Frontend) - ↓ +↓ HTTP Request - ↓ +↓ request.data = { - "name": "Wireless Mouse", - "description": "2.4 GHz ergonomic mouse", - "category": "Electronics", - "price": "799.00", - "brand": "Logitech", - "quantity": 25 +"name": "Wireless Mouse", +"description": "2.4 GHz ergonomic mouse", +"category": "Electronics", +"price": "799.00", +"brand": "Logitech", +"quantity": 25 } - ↓ +↓ django_app/urls.py - ↓ +↓ week2/urls.py - ↓ +↓ views.py - ↓ +↓ serializers.py - ↓ +↓ serializer.validated_data = { - "name": "Wireless Mouse", - "description": "2.4 GHz ergonomic mouse", - "category": "Electronics", - "price": Decimal("799.00"), - "brand": "Logitech", - "quantity": 25 +"name": "Wireless Mouse", +"description": "2.4 GHz ergonomic mouse", +"category": "Electronics", +"price": Decimal("799.00"), +"brand": "Logitech", +"quantity": 25 } - ↓ +↓ store.py - ↓ +↓ Product( - id=1, - name="Wireless Mouse", - description="2.4 GHz ergonomic mouse", - category="Electronics", - price=Decimal("799.00"), - brand="Logitech", - quantity=25 +id=1, +name="Wireless Mouse", +description="2.4 GHz ergonomic mouse", +category="Electronics", +price=Decimal("799.00"), +brand="Logitech", +quantity=25 ) - ↓ +↓ to_dict() ↓ { - "id": 1, - "name": "Wireless Mouse", - "description": "2.4 GHz ergonomic mouse", - "category": "Electronics", - "price": "799.00", - "brand": "Logitech", - "quantity": 25 +"id": 1, +"name": "Wireless Mouse", +"description": "2.4 GHz ergonomic mouse", +"category": "Electronics", +"price": "799.00", +"brand": "Logitech", +"quantity": 25 } - ↓ +↓ serializer.data / Response(...) - ↓ +↓ JSON Response back to client --- @@ -266,6 +265,8 @@ These are the essential tools you need: ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + ``` + ```` 2. **Python 3.14** (3.12 or higher required) @@ -683,3 +684,4 @@ docker compose down # Stop MongoDB docker compose ps # List running containers docker compose logs -f # View logs ``` +```` From e9c17fbced0a7a8f4ccaf69590467e77c6bf19b0 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 11 Mar 2026 21:27:39 +0530 Subject: [PATCH 08/83] readme updated --- backend/python/README.md | 87 ++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index e4819992e..c6cd71cb0 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -156,63 +156,65 @@ Includes `week2.urls` under `/week2/`. ## Architecture flow +```text Client (Browser / Postman / Frontend) -↓ + ↓ HTTP Request -↓ + ↓ request.data = { -"name": "Wireless Mouse", -"description": "2.4 GHz ergonomic mouse", -"category": "Electronics", -"price": "799.00", -"brand": "Logitech", -"quantity": 25 + "name": "Wireless Mouse", + "description": "2.4 GHz ergonomic mouse", + "category": "Electronics", + "price": "799.00", + "brand": "Logitech", + "quantity": 25 } -↓ + ↓ django_app/urls.py -↓ + ↓ week2/urls.py -↓ + ↓ views.py -↓ + ↓ serializers.py -↓ + ↓ serializer.validated_data = { -"name": "Wireless Mouse", -"description": "2.4 GHz ergonomic mouse", -"category": "Electronics", -"price": Decimal("799.00"), -"brand": "Logitech", -"quantity": 25 + "name": "Wireless Mouse", + "description": "2.4 GHz ergonomic mouse", + "category": "Electronics", + "price": Decimal("799.00"), + "brand": "Logitech", + "quantity": 25 } -↓ + ↓ store.py -↓ + ↓ Product( -id=1, -name="Wireless Mouse", -description="2.4 GHz ergonomic mouse", -category="Electronics", -price=Decimal("799.00"), -brand="Logitech", -quantity=25 + id=1, + name="Wireless Mouse", + description="2.4 GHz ergonomic mouse", + category="Electronics", + price=Decimal("799.00"), + brand="Logitech", + quantity=25 ) -↓ + ↓ to_dict() ↓ { -"id": 1, -"name": "Wireless Mouse", -"description": "2.4 GHz ergonomic mouse", -"category": "Electronics", -"price": "799.00", -"brand": "Logitech", -"quantity": 25 + "id": 1, + "name": "Wireless Mouse", + "description": "2.4 GHz ergonomic mouse", + "category": "Electronics", + "price": "799.00", + "brand": "Logitech", + "quantity": 25 } -↓ + ↓ serializer.data / Response(...) -↓ + ↓ JSON Response back to client +``` --- @@ -267,7 +269,9 @@ These are the essential tools you need: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` -```` +``` + +``` 2. **Python 3.14** (3.12 or higher required) @@ -684,4 +688,7 @@ docker compose down # Stop MongoDB docker compose ps # List running containers docker compose logs -f # View logs ``` -```` + +``` + +``` From 653a89cc1105fed34f0787b6979f407ec7676a0e Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 11 Mar 2026 21:29:00 +0530 Subject: [PATCH 09/83] readme updated --- backend/python/README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index c6cd71cb0..011b5f5a8 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -2,9 +2,10 @@ ### What I built -- A **GET endpoint** that returns a greeting message using a query parameter: - - `GET /hello-world/` → `{"message": "Hello, World!"}` - - `GET /hello-world/?name=Kreesh` → `{"message": "Hello, Kreesh!"}` +A **GET endpoint** that returns a greeting message using a query parameter: + +- `GET /hello-world/` → `{"message": "Hello, World!"}` +- `GET /hello-world/?name=Kreesh` → `{"message": "Hello, Kreesh!"}` ### Hexagonal architecture overview From dc274bf9b889d4bda7feb9cafbfaa1a69c3fbf98 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 16 Mar 2026 16:42:51 +0530 Subject: [PATCH 10/83] readme updated --- backend/python/README.md | 2 +- backend/python/week3/__init__.py | 0 backend/python/week3/admin.py | 3 +++ backend/python/week3/apps.py | 5 +++++ backend/python/week3/migrations/__init__.py | 0 backend/python/week3/models.py | 3 +++ backend/python/week3/tests.py | 3 +++ backend/python/week3/views.py | 3 +++ 8 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 backend/python/week3/__init__.py create mode 100644 backend/python/week3/admin.py create mode 100644 backend/python/week3/apps.py create mode 100644 backend/python/week3/migrations/__init__.py create mode 100644 backend/python/week3/models.py create mode 100644 backend/python/week3/tests.py create mode 100644 backend/python/week3/views.py diff --git a/backend/python/README.md b/backend/python/README.md index 011b5f5a8..17d8c3194 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -20,7 +20,7 @@ The implementation is organized into clear layers to keep business logic indepen # WEEK 2 -## What I built +### What I built In Week 2, I built a **Product CRUD API** using **Django REST Framework (DRF)**. diff --git a/backend/python/week3/__init__.py b/backend/python/week3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week3/admin.py b/backend/python/week3/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/python/week3/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/python/week3/apps.py b/backend/python/week3/apps.py new file mode 100644 index 000000000..7da5c93c9 --- /dev/null +++ b/backend/python/week3/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class Week3Config(AppConfig): + name = "week3" diff --git a/backend/python/week3/migrations/__init__.py b/backend/python/week3/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week3/models.py b/backend/python/week3/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/backend/python/week3/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/python/week3/tests.py b/backend/python/week3/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/python/week3/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/python/week3/views.py b/backend/python/week3/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/backend/python/week3/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 47b7ee052602f83ec1d73d2bc82ba2106efc666a Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 17 Mar 2026 15:49:07 +0530 Subject: [PATCH 11/83] week-3 updated --- backend/python/django_app/settings.py | 1 + backend/python/django_app/urls.py | 1 + backend/python/week3/apps.py | 5 +++ backend/python/week3/db_connection.py | 22 +++++++++++ backend/python/week3/models.py | 32 +++++++++++++++- backend/python/week3/repository.py | 55 +++++++++++++++++++++++++++ backend/python/week3/serializers.py | 40 +++++++++++++++++++ backend/python/week3/service.py | 22 +++++++++++ backend/python/week3/urls.py | 9 +++++ backend/python/week3/views.py | 3 -- 10 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 backend/python/week3/db_connection.py create mode 100644 backend/python/week3/repository.py create mode 100644 backend/python/week3/serializers.py create mode 100644 backend/python/week3/service.py create mode 100644 backend/python/week3/urls.py diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index 464f9b7ec..04bd688af 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -39,6 +39,7 @@ "django.contrib.staticfiles", "rest_framework", "week2", + "week3.apps.Week3Config", ] MIDDLEWARE = [ diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index 3d67b7df8..b8f4581cd 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -22,4 +22,5 @@ def hello_name(request): # returns {"message": "Hello, Bob!"} path("", include("django_app.adapters.api.urls")), path("week2/", include("week2.urls")), + path("api/week3/", include("week3.urls")), ] diff --git a/backend/python/week3/apps.py b/backend/python/week3/apps.py index 7da5c93c9..c07e9c723 100644 --- a/backend/python/week3/apps.py +++ b/backend/python/week3/apps.py @@ -1,5 +1,10 @@ from django.apps import AppConfig +from .db_connection import initialize_mongo class Week3Config(AppConfig): + default_auto_field = "django.db.models.BigAutoField" name = "week3" + + def ready(self): + initialize_mongo() diff --git a/backend/python/week3/db_connection.py b/backend/python/week3/db_connection.py new file mode 100644 index 000000000..ee027b75f --- /dev/null +++ b/backend/python/week3/db_connection.py @@ -0,0 +1,22 @@ +import os +from dotenv import load_dotenv +from mongoengine import connect + +load_dotenv() + + +def initialize_mongo(): + mongo_user = os.getenv("MONGO_USER", "root") + mongo_pass = os.getenv("MONGO_PASS", "example") + mongo_host = os.getenv("MONGO_HOST", "localhost") + mongo_port = os.getenv("MONGO_PORT", "27019") + mongo_db = os.getenv("MONGO_DB", "interneers_lab_week3") + + mongo_uri = (f"mongodb://{mongo_user}:{mongo_pass}" + f"@{mongo_host}:{mongo_port}/{mongo_db}?authSource=admin") + + connect( + db=mongo_db, + host=mongo_uri, + alias="default", + ) diff --git a/backend/python/week3/models.py b/backend/python/week3/models.py index 71a836239..7e965bb16 100644 --- a/backend/python/week3/models.py +++ b/backend/python/week3/models.py @@ -1,3 +1,31 @@ -from django.db import models +from datetime import datetime +from mongoengine import Document, SequenceField, StringField, DecimalField, IntField, DateTimeField -# Create your models here. + +class Product(Document): + id = SequenceField(primary_key=True) + name = StringField(required=True, max_length=200) + description = StringField(required=True) + category = StringField(required=True, max_length=100) + price = DecimalField(required=True, precision=2, min_value=0) + brand = StringField(max_length=100, default="") + quantity = IntField(required=True, min_value=0) + created_at = DateTimeField(default=datetime.utcnow) + updated_at = DateTimeField(default=datetime.utcnow) + + meta = { + "collection": "products" + } + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "description": self.description, + "category": self.category, + "price": str(self.price), + "brand": self.brand, + "quantity": self.quantity, + "created_at": self.created_at, + "updated_at": self.updated_at, + } diff --git a/backend/python/week3/repository.py b/backend/python/week3/repository.py new file mode 100644 index 000000000..5c491e597 --- /dev/null +++ b/backend/python/week3/repository.py @@ -0,0 +1,55 @@ +# replacement for store in week 2 +# here i replaced in memory store with repository layer having same CRUD interface but with mongoDB using mongoengine +from datetime import datetime +from .models import Product + + +class ProductRepository: + # create new product + def create(self, data): + product = Product( + name=data["name"], + description=data["description"], + category=data["category"], + price=data["price"], + brand=data.get("brand", ""), + quantity=data["quantity"], + updated_at=datetime.utcnow(), + ) + product.save() + return product + + # get specific product by id + def get(self, id): + return Product.objects(id=id).first() + + # list all products + def list_all(self): + return Product.objects.all() + + # update product + def update(self, id, data): + product = self.get(id) + if not product: + return None + + product.name = data["name"] + product.description = data["description"] + product.category = data["category"] + product.price = data["price"] + product.brand = data.get("brand", "") + product.quantity = data["quantity"] + product.updated_at = datetime.utcnow() + product.save() + return product + + # delete product + def delete(self, id): + product = self.get(id) + if not product: + return False + product.delete() + return True + + +product_repository = ProductRepository() diff --git a/backend/python/week3/serializers.py b/backend/python/week3/serializers.py new file mode 100644 index 000000000..be64434ec --- /dev/null +++ b/backend/python/week3/serializers.py @@ -0,0 +1,40 @@ +# same as week 2 +from rest_framework import serializers + + +class ProductSerializer(serializers.Serializer): + product_id = serializers.IntegerField(read_only=True) + name = serializers.CharField(max_length=200) + description = serializers.CharField() + category = serializers.CharField(max_length=100) + price = serializers.DecimalField(max_digits=10, decimal_places=2) + brand = serializers.CharField( + max_length=100, required=False, allow_blank=True) + quantity = serializers.IntegerField() + created_at = serializers.DateTimeField(read_only=True) + updated_at = serializers.DateTimeField(read_only=True) + + def validate_name(self, value): + if not value.strip(): + raise serializers.ValidationError("Name cannot be empty.") + return value + + def validate_category(self, value): + if not value.strip(): + raise serializers.ValidationError("Category cannot be empty.") + return value + + def validate_price(self, value): + if value <= 0: + raise serializers.ValidationError("Price must be greater than 0.") + return value + + def validate_quantity(self, value): + if value < 0: + raise serializers.ValidationError("Quantity cannot be negative.") + return value + + def validate_brand(self, value): + if value != "" and not value.strip(): + raise serializers.ValidationError("Brand cannot be only spaces.") + return value diff --git a/backend/python/week3/service.py b/backend/python/week3/service.py new file mode 100644 index 000000000..3e97b7a13 --- /dev/null +++ b/backend/python/week3/service.py @@ -0,0 +1,22 @@ +# bridge between views and repository +from .repository import product_repository + + +class ProductService: + def create(self, data): + return product_repository.create(data) + + def get(self, id): + return product_repository.get(id) + + def list_all(self): + return product_repository.list_all() + + def update(self, id, data): + return product_repository.update(id, data) + + def delete(self, id): + return product_repository.delete(id) + + +product_service = ProductService() diff --git a/backend/python/week3/urls.py b/backend/python/week3/urls.py new file mode 100644 index 000000000..02b1c2c28 --- /dev/null +++ b/backend/python/week3/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import ProductDetailAPIView, ProductListCreateAPIView + +urlpatterns = [ + path("products/", ProductListCreateAPIView.as_view(), + name="product-list-create"), + path("products//", + ProductDetailAPIView.as_view(), name="product-detail"), +] diff --git a/backend/python/week3/views.py b/backend/python/week3/views.py index 91ea44a21..e69de29bb 100644 --- a/backend/python/week3/views.py +++ b/backend/python/week3/views.py @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From a6f021d687d9b005816e8edcc5ea3e7c3decb92c Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 17 Mar 2026 16:33:46 +0530 Subject: [PATCH 12/83] week-3 updated --- backend/python/week3/serializers.py | 2 +- backend/python/week3/urls.py | 2 +- backend/python/week3/views.py | 62 +++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/backend/python/week3/serializers.py b/backend/python/week3/serializers.py index be64434ec..5f34b36f1 100644 --- a/backend/python/week3/serializers.py +++ b/backend/python/week3/serializers.py @@ -3,7 +3,7 @@ class ProductSerializer(serializers.Serializer): - product_id = serializers.IntegerField(read_only=True) + id = serializers.IntegerField(read_only=True) name = serializers.CharField(max_length=200) description = serializers.CharField() category = serializers.CharField(max_length=100) diff --git a/backend/python/week3/urls.py b/backend/python/week3/urls.py index 02b1c2c28..c94af330c 100644 --- a/backend/python/week3/urls.py +++ b/backend/python/week3/urls.py @@ -4,6 +4,6 @@ urlpatterns = [ path("products/", ProductListCreateAPIView.as_view(), name="product-list-create"), - path("products//", + path("products//", ProductDetailAPIView.as_view(), name="product-detail"), ] diff --git a/backend/python/week3/views.py b/backend/python/week3/views.py index e69de29bb..36d5c5cf5 100644 --- a/backend/python/week3/views.py +++ b/backend/python/week3/views.py @@ -0,0 +1,62 @@ +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from .serializers import ProductSerializer +from .service import product_service + + +class ProductListCreateAPIView(APIView): + # list all product + def get(self, request): + products = product_service.list_all() + product_data = [] + for product in products: + product_data.append(product.to_dict()) + + serializer = ProductSerializer(product_data, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) + + # create a product + def post(self, request): + serializer = ProductSerializer(data=request.data) + if serializer.is_valid(): + product = product_service.create(serializer.validated_data) + response_serializer = ProductSerializer(product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ProductDetailAPIView(APIView): + # get one product + def get(self, request, id): + product = product_service.get(id) + if not product: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) + else: + serializer = ProductSerializer(product.to_dict()) + return Response(serializer.data, status=status.HTTP_200_OK) + + # update one product + def put(self, request, id): + existing_product = product_service.get(id) + if not existing_product: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) + else: + serializer = ProductSerializer(data=request.data) + if serializer.is_valid(): + updated_product = product_service.update( + id, serializer.validated_data) + response_serializer = ProductSerializer( + updated_product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # delete product + def delete(self, request, product_id): + deleted = product_service.delete(product_id) + if not deleted: + return Response({"error": "Product not found."}, status=status.HTTP_404_NOT_FOUND) + else: + return Response(status=status.HTTP_204_NO_CONTENT) From 55b92222004ba11ab73aabd113e77402dfe00933 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 17 Mar 2026 20:09:40 +0530 Subject: [PATCH 13/83] week-3 updated --- .../.db_connection.py_e2e76d89979cabfcefabda2cc23e759c.prob | 1 + backend/python/week3/db_connection.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 backend/python/week3/.cph/.db_connection.py_e2e76d89979cabfcefabda2cc23e759c.prob diff --git a/backend/python/week3/.cph/.db_connection.py_e2e76d89979cabfcefabda2cc23e759c.prob b/backend/python/week3/.cph/.db_connection.py_e2e76d89979cabfcefabda2cc23e759c.prob new file mode 100644 index 000000000..d1077f0a2 --- /dev/null +++ b/backend/python/week3/.cph/.db_connection.py_e2e76d89979cabfcefabda2cc23e759c.prob @@ -0,0 +1 @@ +{"name":"Local: db_connection","url":"c:\\Users\\Kreesh\\OneDrive\\Desktop\\Rippling\\interneers-lab\\backend\\python\\week3\\db_connection.py","tests":[{"id":1773758364094,"input":"","output":""}],"interactive":false,"memoryLimit":1024,"timeLimit":3000,"srcPath":"c:\\Users\\Kreesh\\OneDrive\\Desktop\\Rippling\\interneers-lab\\backend\\python\\week3\\db_connection.py","group":"local","local":true} \ No newline at end of file diff --git a/backend/python/week3/db_connection.py b/backend/python/week3/db_connection.py index ee027b75f..86362bf86 100644 --- a/backend/python/week3/db_connection.py +++ b/backend/python/week3/db_connection.py @@ -1,3 +1,5 @@ +# bridge between django file and mongoDB + import os from dotenv import load_dotenv from mongoengine import connect From a95e8e692b5786fb4e7f2aba9471051e96aa027d Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 17 Mar 2026 22:30:39 +0530 Subject: [PATCH 14/83] week-3 updated --- backend/python/week3/apps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/python/week3/apps.py b/backend/python/week3/apps.py index c07e9c723..3a7f6e0ea 100644 --- a/backend/python/week3/apps.py +++ b/backend/python/week3/apps.py @@ -3,7 +3,6 @@ class Week3Config(AppConfig): - default_auto_field = "django.db.models.BigAutoField" name = "week3" def ready(self): From 9360c5b6053ac4c2cc9646f48699459209e3d705 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 17 Mar 2026 22:53:11 +0530 Subject: [PATCH 15/83] week-3 updated --- backend/python/week3/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/python/week3/models.py b/backend/python/week3/models.py index 7e965bb16..447b0631f 100644 --- a/backend/python/week3/models.py +++ b/backend/python/week3/models.py @@ -3,6 +3,7 @@ class Product(Document): + # seq field is used for auto incrementing numeric id's id = SequenceField(primary_key=True) name = StringField(required=True, max_length=200) description = StringField(required=True) @@ -13,6 +14,7 @@ class Product(Document): created_at = DateTimeField(default=datetime.utcnow) updated_at = DateTimeField(default=datetime.utcnow) + # you are telling MongoEngine to put all Product objects inside the products folder/bucket meta = { "collection": "products" } From 1b66261034307c76392a443a2792ca40793aa90f Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 17 Mar 2026 23:30:36 +0530 Subject: [PATCH 16/83] week-3 updated --- backend/python/django_app/urls.py | 2 +- backend/python/week3/repository.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index b8f4581cd..a49302744 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -22,5 +22,5 @@ def hello_name(request): # returns {"message": "Hello, Bob!"} path("", include("django_app.adapters.api.urls")), path("week2/", include("week2.urls")), - path("api/week3/", include("week3.urls")), + path("week3/", include("week3.urls")), ] diff --git a/backend/python/week3/repository.py b/backend/python/week3/repository.py index 5c491e597..e3c6c78be 100644 --- a/backend/python/week3/repository.py +++ b/backend/python/week3/repository.py @@ -16,6 +16,8 @@ def create(self, data): quantity=data["quantity"], updated_at=datetime.utcnow(), ) + # tells mongoengine to persist this document in mongodb + # so mongoengine looks at the model, sees meta = {"" : ""} connects through default mongodb connection and inserts an new doc into the product collection product.save() return product From 7c98808be8373b87cc1f58cb4d6fdc4ddfa9dc18 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 18 Mar 2026 00:20:15 +0530 Subject: [PATCH 17/83] readme-updated --- backend/python/README.md | 146 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/backend/python/README.md b/backend/python/README.md index 17d8c3194..25407ac16 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -219,6 +219,152 @@ JSON Response back to client --- +# Week 3 + +## What I Built + +In Week 3, I refactored the Week 2 in-memory Product CRUD API into a more structured backend architecture using a thin controller layer, service layer, repository layer, MongoDB for persistent storage, and MongoEngine for model-based database interaction. + +Unlike Week 2, where products were stored only in memory, this version stores product data in MongoDB, so data persists even after restarting the server. + +--- + +## Architecture Flows + +### Flow 1 — Request Handling Flow + +```text +Client (Browser / Postman / Frontend) + ↓ +HTTP Request + ↓ +django_app/urls.py + ↓ +week3/urls.py + ↓ +views.py + ↓ +serializers.py + ↓ +services.py + ↓ +repository.py + ↓ +models.py + ↓ +MongoDB + ↓ +repository.py + ↓ +services.py + ↓ +views.py + ↓ +DRF Response + ↓ +JSON Response back to client +``` + +--- + +### Flow 2 — Technology Relationship Flow + +```text +Client + ↓ +Django / DRF + ↓ +View Layer + ↓ +Service Layer + ↓ +Repository Layer + ↓ +MongoEngine + ↓ +PyMongo + ↓ +MongoDB Server + ↓ +MongoDB Compass +``` + +--- + +### Flow 3 — Example Product Creation Flow + +```text +Client (Postman / Frontend) + ↓ +HTTP POST /week3/products/ + ↓ +request.data = { + "name": "Printer", + "description": "B/W printer", + "category": "Electronics", + "price": "8000.00", + "brand": "HP", + "quantity": 6 +} + ↓ +django_app/urls.py → week3/urls.py → views.py + ↓ +serializers.py + ↓ +serializer.validated_data = { + "name": "Printer", + "description": "B/W printer", + "category": "Electronics", + "price": Decimal("8000.00"), + "brand": "HP", + "quantity": 6 +} + ↓ +services.py → repository.py + ↓ +product.save() + ↓ +Stored in MongoDB + ↓ +{ + "_id": ObjectId("..."), + "name": "Printer", + ... +} + ↓ +JSON Response back to client + ↓ +{ + "id": "", + "name": "Printer", + "description": "B/W printer", + "category": "Electronics", + "price": "8000.00", + "brand": "HP", + "quantity": 6 +} +``` + +--- + +## Example Endpoints + +| Method | Endpoint | Description | +| ------------- | ----------------------- | ------------------ | +| `POST` | `/week3/products/` | Create product | +| `GET` | `/week3/products/` | Fetch all products | +| `GET` | `/week3/products//` | Fetch one product | +| `PUT`/`PATCH` | `/week3/products//` | Update product | +| `DELETE` | `/week3/products//` | Delete product | + +--- + +## Final Takeaway + +Week 3 was about moving from a basic CRUD project to a realistic, layered backend architecture — separating responsibilities into view, service, repository, and model layers, keeping controllers thin, and using MongoDB for real persistence. + +--- + # Interneers Lab - Backend in Python Welcome to the **Interneers Lab 2026** Python backend! This serves as a minimal starter kit for learning and experimenting with: From 5dd099053d4915157e0ef07b1a6fc0a6f6f30b58 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 18 Mar 2026 00:27:37 +0530 Subject: [PATCH 18/83] readme-updated --- backend/python/README.md | 86 +++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 49 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index 25407ac16..c5c85edce 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -267,82 +267,70 @@ JSON Response back to client --- -### Flow 2 — Technology Relationship Flow +### Flow 2 — Docker Compose Flow ```text -Client +docker compose up -d + ↓ +Read compose.yml + ↓ +Find services: + - app + - db + ↓ +Create default network + ↓ +Create db volume ↓ -Django / DRF +Pull postgres image ↓ -View Layer +Build app image from Dockerfile ↓ -Service Layer +Create db container ↓ -Repository Layer +Start db container ↓ -MongoEngine +Create app container ↓ -PyMongo +Start app container ↓ -MongoDB Server +app talks to db using service name: db ↓ -MongoDB Compass +Both keep running in background ``` --- -### Flow 3 — Example Product Creation Flow +## Flow 3 — Django Server Startup Flow ```text -Client (Postman / Frontend) +python manage.py runserver ↓ -HTTP POST /week3/products/ +manage.py sets DJANGO_SETTINGS_MODULE ↓ -request.data = { - "name": "Printer", - "description": "B/W printer", - "category": "Electronics", - "price": "8000.00", - "brand": "HP", - "quantity": 6 -} +django.core.management executes the runserver command ↓ -django_app/urls.py → week3/urls.py → views.py +Django imports settings.py ↓ -serializers.py +App configuration is loaded ↓ -serializer.validated_data = { - "name": "Printer", - "description": "B/W printer", - "category": "Electronics", - "price": Decimal("8000.00"), - "brand": "HP", - "quantity": 6 -} +A MongoDB setup module / connection block is imported ↓ -services.py → repository.py +mongoengine.connect(...) ↓ -product.save() +Connection is established to the MongoDB container/server ↓ -Stored in MongoDB +MongoEngine Document models become usable ↓ -{ - "_id": ObjectId("..."), - "name": "Printer", - ... -} +Repository layer can now perform DB operations ↓ -JSON Response back to client +Service layer can call repository methods ↓ -{ - "id": "", - "name": "Printer", - "description": "B/W printer", - "category": "Electronics", - "price": "8000.00", - "brand": "HP", - "quantity": 6 -} +View layer can safely handle API requests + ↓ +Server startup completes + ↓ +Application is ready for Product CRUD with MongoDB persistence ``` --- From 7e4868205b5d5c2beaca5f369b6add70776b9f8f Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 18 Mar 2026 00:29:55 +0530 Subject: [PATCH 19/83] readme-updated --- backend/python/README.md | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index c5c85edce..189b97961 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -61,43 +61,6 @@ In this project: --- -## Quick theory: HTTP verbs used - -### GET - -Used to fetch data. - -Examples: - -- `GET /week2/products/` -- `GET /week2/products/1/` - -### POST - -Used to create new data. - -Example: - -- `POST /week2/products/` - -### PUT - -Used to update existing data fully. - -Example: - -- `PUT /week2/products/1/` - -### DELETE - -Used to remove data. - -Example: - -- `DELETE /week2/products/1/` - ---- - ## Week 2 architecture overview The Week 2 implementation is split into small layers so each file has one responsibility. From 72c453d855cbd2b43c7053a5b41a6ce2d94e0808 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 18 Mar 2026 21:07:28 +0530 Subject: [PATCH 20/83] week-3 completed --- backend/python/week3/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/python/week3/views.py b/backend/python/week3/views.py index 36d5c5cf5..9af53aaba 100644 --- a/backend/python/week3/views.py +++ b/backend/python/week3/views.py @@ -1,3 +1,4 @@ +# same as week 2, only request passes through service rather than directly to store from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView From 075af1242647a198c6e5d5ddc82c5149b54e2f30 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Wed, 18 Mar 2026 21:48:21 +0530 Subject: [PATCH 21/83] week4- started --- backend/python/week4/__init__.py | 0 backend/python/week4/admin.py | 3 +++ backend/python/week4/apps.py | 5 +++++ backend/python/week4/migrations/__init__.py | 0 backend/python/week4/models.py | 3 +++ backend/python/week4/tests.py | 3 +++ backend/python/week4/views.py | 3 +++ 7 files changed, 17 insertions(+) create mode 100644 backend/python/week4/__init__.py create mode 100644 backend/python/week4/admin.py create mode 100644 backend/python/week4/apps.py create mode 100644 backend/python/week4/migrations/__init__.py create mode 100644 backend/python/week4/models.py create mode 100644 backend/python/week4/tests.py create mode 100644 backend/python/week4/views.py diff --git a/backend/python/week4/__init__.py b/backend/python/week4/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week4/admin.py b/backend/python/week4/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/backend/python/week4/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/python/week4/apps.py b/backend/python/week4/apps.py new file mode 100644 index 000000000..19cddc551 --- /dev/null +++ b/backend/python/week4/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class Week4Config(AppConfig): + name = "week4" diff --git a/backend/python/week4/migrations/__init__.py b/backend/python/week4/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week4/models.py b/backend/python/week4/models.py new file mode 100644 index 000000000..71a836239 --- /dev/null +++ b/backend/python/week4/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/python/week4/tests.py b/backend/python/week4/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/backend/python/week4/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/python/week4/views.py b/backend/python/week4/views.py new file mode 100644 index 000000000..91ea44a21 --- /dev/null +++ b/backend/python/week4/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From 1a7b2119c0152028ea965601a678db4fc0dde40f Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 21 Mar 2026 18:12:00 +0530 Subject: [PATCH 22/83] week2 - Pagination added --- backend/python/django_app/settings.py | 1 + backend/python/week2/views.py | 58 ++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index 04bd688af..b1b3a4e47 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -40,6 +40,7 @@ "rest_framework", "week2", "week3.apps.Week3Config", + "django_filters", ] MIDDLEWARE = [ diff --git a/backend/python/week2/views.py b/backend/python/week2/views.py index 0e3af5ba6..20841c8d4 100644 --- a/backend/python/week2/views.py +++ b/backend/python/week2/views.py @@ -12,16 +12,64 @@ class ProductListCreateAPIView(APIView): # list all product def get(self, request): products = product_store.list_all() + # manual pagination + page = request.query_params.get("page", "1") + page_size = request.query_params.get("page_size", "5") + if not page.isdigit() or not page_size.isdigit(): + return Response( + {"error": "page and page_size must be integers"}, status=status.HTTP_400_BAD_REQUEST + ) + + page = int(page) + page_size = int(page_size) + if page <= 0 or page_size <= 0: + return Response( + {"error": "page and page_size must be greater than 0"}, status=status.HTTP_400_BAD_REQUEST + ) + + total_items = len(products) + start = (page-1)*page_size + end = start + page_size + paginated_products = products[start: end] product_data = [] - for product in products: + + for product in paginated_products: product_data.append(product.to_dict()) - # output serialisation - # without many = true DRF would expect one product + serializer = ProductSerializer(product_data, many=True) - # Response(product_data, status=status.HTTP_200_OK) is also valid - return Response(serializer.data, status=status.HTTP_200_OK) + + if end < total_items: + next_page = page + 1 + else: + next_page = None + + if page > 1: + previous_page = page-1 + else: + previous_page = None + + return Response( + { + "count": total_items, + "page": page, + "page_size": page_size, + "next_page": next_page, + "previous_page": previous_page, + "results": serializer.data, + }, status=status.HTTP_200_OK + ) + + # product_data = [] + # for product in products: + # product_data.append(product.to_dict()) + # # output serialisation + # # without many = true DRF would expect one product + # serializer = ProductSerializer(product_data, many=True) + # # Response(product_data, status=status.HTTP_200_OK) is also valid + # return Response(serializer.data, status=status.HTTP_200_OK) # create a product + def post(self, request): # input serialisation serializer = ProductSerializer(data=request.data) From 7ec0fd76167696242a0138deff2ca0cc9bc5416e Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 21 Mar 2026 18:28:16 +0530 Subject: [PATCH 23/83] week3 - completed --- backend/python/week3/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/python/week3/views.py b/backend/python/week3/views.py index 9af53aaba..151b1161c 100644 --- a/backend/python/week3/views.py +++ b/backend/python/week3/views.py @@ -55,8 +55,8 @@ def put(self, request, id): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # delete product - def delete(self, request, product_id): - deleted = product_service.delete(product_id) + def delete(self, request, id): + deleted = product_service.delete(id) if not deleted: return Response({"error": "Product not found."}, status=status.HTTP_404_NOT_FOUND) else: From 646e03905da63a4e40f0a94dd0fbf0bb7804a4c0 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 21 Mar 2026 18:34:55 +0530 Subject: [PATCH 24/83] week4 - updated --- backend/python/week4/apps.py | 4 ++++ backend/python/week4/db_connection.py | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 backend/python/week4/db_connection.py diff --git a/backend/python/week4/apps.py b/backend/python/week4/apps.py index 19cddc551..14430f577 100644 --- a/backend/python/week4/apps.py +++ b/backend/python/week4/apps.py @@ -1,5 +1,9 @@ from django.apps import AppConfig +from .db_connection import initialize_mongo class Week4Config(AppConfig): name = "week4" + + def ready(self): + initialize_mongo() diff --git a/backend/python/week4/db_connection.py b/backend/python/week4/db_connection.py new file mode 100644 index 000000000..73df8eeab --- /dev/null +++ b/backend/python/week4/db_connection.py @@ -0,0 +1,24 @@ +import os +from dotenv import load_dotenv +from mongoengine import connect + +load_dotenv() + + +def initialize_mongo(): + mongo_user = os.getenv("MONGO_USER", "root") + mongo_pass = os.getenv("MONGO_PASS", "example") + mongo_host = os.getenv("MONGO_HOST", "localhost") + mongo_port = os.getenv("MONGO_PORT", "27019") + mongo_db = os.getenv("MONGO_DB", "interneers_lab_week4") + + mongo_uri = ( + f"mongodb://{mongo_user}:{mongo_pass}" + f"@{mongo_host}:{mongo_port}/{mongo_db}?authSource=admin" + ) + + connect( + db=mongo_db, + host=mongo_uri, + alias="default", + ) From 07efff73c98cd5c6350c321bc9c4e8299a85060c Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 00:31:08 +0530 Subject: [PATCH 25/83] week4 - updated --- backend/python/week4/csv_helper.py | 0 backend/python/week4/repository.py | 0 backend/python/week4/serializers.py | 0 backend/python/week4/service.py | 0 backend/python/week4/urls.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/python/week4/csv_helper.py create mode 100644 backend/python/week4/repository.py create mode 100644 backend/python/week4/serializers.py create mode 100644 backend/python/week4/service.py create mode 100644 backend/python/week4/urls.py diff --git a/backend/python/week4/csv_helper.py b/backend/python/week4/csv_helper.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week4/repository.py b/backend/python/week4/repository.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week4/serializers.py b/backend/python/week4/serializers.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week4/service.py b/backend/python/week4/service.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week4/urls.py b/backend/python/week4/urls.py new file mode 100644 index 000000000..e69de29bb From 2cbd900783e54d21e25ab51464f26720b481443b Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 05:50:08 +0530 Subject: [PATCH 26/83] week4 - csv_helper created --- backend/python/week4/csv_helper.py | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/backend/python/week4/csv_helper.py b/backend/python/week4/csv_helper.py index e69de29bb..0c2e740e7 100644 --- a/backend/python/week4/csv_helper.py +++ b/backend/python/week4/csv_helper.py @@ -0,0 +1,49 @@ +import csv +import io + + +def read_csv_file(uploaded_file, requiered_column=None): + # check if file exists + if uploaded_file is None: + raise ValueError("No file was uploaded") + + # check that file is not empty + file_size = uploaded_file.read() + if not file_size: + raise ValueError("CSV file is empty") + + # decode it into text + try: + decoded_file = file_size.decode("utf-8") + except UnicodeDecodeError: + raise ValueError("CSV file not properly formatted") + + # make text behave like file + file_stream = io.StringIO(decoded_file) + + # parsing + reader = csv.DictReader(file_stream) + + if reader.fieldnames is None: + raise ValueError("CSV file is missing header row") + + # removing spaces in header + cleaned_fieldnames = [] + + for field in reader.fieldnames: + cleaned_field = field.strip() + cleaned_fieldnames.append(cleaned_field) + + reader.fieldnames = cleaned_field + + if requiered_column: + missing_column = [] + for col in requiered_column: + if col not in reader.fieldnames: + missing_column.append(col) + # returning which headers were missing + if missing_column: + raise ValueError( + f"Missing CSV columns : {', '.join(missing_column)}") + + return reader From c516a7126c7ca67d89468f6be3547084e372510d Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 05:53:05 +0530 Subject: [PATCH 27/83] week4 - csv_helper created --- backend/python/week4/csv_helper.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/python/week4/csv_helper.py b/backend/python/week4/csv_helper.py index 0c2e740e7..eded9b4db 100644 --- a/backend/python/week4/csv_helper.py +++ b/backend/python/week4/csv_helper.py @@ -2,7 +2,7 @@ import io -def read_csv_file(uploaded_file, requiered_column=None): +def read_csv_file(uploaded_file, requiered_columns=None): # check if file exists if uploaded_file is None: raise ValueError("No file was uploaded") @@ -34,11 +34,11 @@ def read_csv_file(uploaded_file, requiered_column=None): cleaned_field = field.strip() cleaned_fieldnames.append(cleaned_field) - reader.fieldnames = cleaned_field + reader.fieldnames = cleaned_fieldnames - if requiered_column: + if requiered_columns: missing_column = [] - for col in requiered_column: + for col in requiered_columns: if col not in reader.fieldnames: missing_column.append(col) # returning which headers were missing From d7401c990e393ea4ef4937c64a399343cd96ac6d Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 06:29:58 +0530 Subject: [PATCH 28/83] week4 - models created --- backend/python/week4/models.py | 65 ++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/backend/python/week4/models.py b/backend/python/week4/models.py index 71a836239..858faa477 100644 --- a/backend/python/week4/models.py +++ b/backend/python/week4/models.py @@ -1,3 +1,64 @@ -from django.db import models +from datetime import datetime +from mongoengine import Document, SequenceField, StringField, DecimalField, IntField, DateTimeField, ReferenceField, NULLIFY -# Create your models here. +# one category can contain many products but one product belongs to one category + + +# this represents a category like food, kitchen essentials, electornics, etc +class ProductCategory(Document): + id = SequenceField(primary_key=True) + # no 2 category should have same title + title = StringField(required=True, unique=True, max_length=100) + description = StringField(default="", max_length=300) + created_at = DateTimeField(default=datetime.utcnow) + updated_at = DateTimeField(default=datetime.utcnow) + + # just like week3 this tells mongoengine which mongodb collection name to use + meta = { + "collection": "product_categories" + } + + def to_dict(self): + return { + "id": self.id, + "title": self.title, + "description": self.description, + "created_at": self.created_at, + "updated_at": self.updated_at, + } + + +# this represents actual product like TV, fridge, rice, etc +class Product(Document): + id = SequenceField(primary_key=True) + name = StringField(required=True, max_length=200) + description = StringField(required=True) + price = DecimalField(required=True, precision=2, min_value=0) + # at model level, brand is optional because old products may already exist without brand + # if model makes it compulsary then old record may cause problems + # for new products we have enforced rules in serializer and service + brand = StringField(required=False, max_length=100, default="") + quantity = IntField(required=True, min_value=0) + # reverse delete rule = null bcz if ref category is deleted do not delete product + category = ReferenceField( + ProductCategory, required=False, null=True, reverse_delete_rule=NULLIFY) + created_at = DateTimeField(default=datetime.utcnow) + updated_at = DateTimeField(default=datetime.utcnow) + + meta = { + "collection": "products" + } + + def to_dict(self): + return { + "id": self.id, + "name": self.name, + "description": self.description, + "price": str(self.price), + "brand": self.brand, + "quantity": self.quantity, + "category": self.category.to_dict() if self.category else None, + "category_id": self.category.id if self.category else None, + "created_at": self.created_at, + "updated_at": self.updated_at, + } From e322336c9e47cc7e800a337deabeae0c8097b7b2 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 07:28:22 +0530 Subject: [PATCH 29/83] week4 - repository created --- backend/python/week4/repository.py | 220 +++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/backend/python/week4/repository.py b/backend/python/week4/repository.py index e69de29bb..dd3020ccd 100644 --- a/backend/python/week4/repository.py +++ b/backend/python/week4/repository.py @@ -0,0 +1,220 @@ +# repository.py : bridge between service and databse +from datetime import datetime +from .models import Product, ProductCategory + + +class ProductCategoryRepository: + # create category + def create(self, data): + category = ProductCategory( + title=data["title"], + description=data.get("description", ""), + updated_at=datetime.utcnow(), + ) + category.save() + return category + + # get category with id + def get(self, id): + return ProductCategory.objects(id=id).first() + + # get category with title + def get_by_title(self, title): + return ProductCategory.objects(title__iexact=title.strip()).first() + + # list all categories + def list_all(self, filters=None, sort_by=None): + queryset = ProductCategory.objects + # repository.py will only do filtering+sorting and returning queryset, pagination is done in views.py + + # manual filtering is added (icontains : case sensitive substring match) + if filters: + if filters.get("title"): + queryset = queryset.filter(title__icontains=filters["title"]) + + # manual sorting is added + allowed_sort_fields = ["id", "title", "created_at", "updated_at"] + if sort_by and sort_by.lstrip("-") in allowed_sort_fields: + queryset = queryset.order_by(sort_by) + else: + queryset = queryset.order_by("id") + + return queryset + + # update category + def update(self, id, data): + category = self.get(id) + if not category: + return None + + category.title = data["title"] + category.description = data.get("description", "") + category.updated_at = datetime.utcnow() + category.save() + return category + + # delete category + def delete(self, id): + category = self.get(id) + if not category: + return False + + category.delete() + return True + + +class ProductRepository: + # create product + def create(self, data): + product = Product( + name=data["name"], + description=data["description"], + price=data["price"], + brand=data["brand"], + quantity=data["quantity"], + category=data.get("category"), + updated_at=datetime.utcnow(), + ) + product.save() + return product + + # get product with id + def get(self, id): + return Product.objects(id=id).first() + + # list all products + def list_all(self, filters=None, sort_by=None): + queryset = Product.objects + # filtering + if filters: + if filters.get("name"): + queryset = queryset.filter(name__icontains=filters["name"]) + + if filters.get("brand"): + queryset = queryset.filter(brand__icontains=filters["brand"]) + + if filters.get("min_price") is not None: + queryset = queryset.filter(price__gte=filters["min_price"]) + + if filters.get("max_price") is not None: + queryset = queryset.filter(price__lte=filters["max_price"]) + + if filters.get("min_quantity") is not None: + queryset = queryset.filter( + quantity__gte=filters["min_quantity"]) + + if filters.get("max_quantity") is not None: + queryset = queryset.filter( + quantity__lte=filters["max_quantity"]) + + if filters.get("category"): + queryset = queryset.filter(category=filters["category"]) + + # sorting + allowed_sort_fields = ["id", "name", "price", + "brand", "quantity", "created_at", "updated_at"] + + if sort_by and sort_by.lstrip("-") in allowed_sort_fields: + queryset = queryset.order_by(sort_by) + else: + queryset = queryset.order_by("id") + + return queryset + + # update product + def update(self, id, data): + product = self.get(id) + if not product: + return None + + product.name = data["name"] + product.description = data["description"] + product.price = data["price"] + product.brand = data["brand"] + product.quantity = data["quantity"] + product.category = data.get("category") + product.updated_at = datetime.utcnow() + product.save() + return product + + # delete product + def delete(self, id): + product = self.get(id) + if not product: + return False + + product.delete() + return True + + # list all products belonging to one category + def list_by_category(self, category, filters=None, sort_by=None): + queryset = Product.objects(category=category) + + if filters: + if filters.get("name"): + queryset = queryset.filter(name__icontains=filters["name"]) + + if filters.get("brand"): + queryset = queryset.filter(brand__icontains=filters["brand"]) + + if filters.get("min_price") is not None: + queryset = queryset.filter(price__gte=filters["min_price"]) + + if filters.get("max_price") is not None: + queryset = queryset.filter(price__lte=filters["max_price"]) + + if filters.get("min_quantity") is not None: + queryset = queryset.filter( + quantity__gte=filters["min_quantity"]) + + if filters.get("max_quantity") is not None: + queryset = queryset.filter( + quantity__lte=filters["max_quantity"]) + + allowed_sort_fields = ["id", "name", "price", + "brand", "quantity", "created_at", "updated_at"] + + if sort_by and sort_by.lstrip("-") in allowed_sort_fields: + queryset = queryset.order_by(sort_by) + else: + queryset = queryset.order_by("id") + + return queryset + + # assign product to category + def assign_to_category(self, product, category): + product.category = category + product.updated_at = datetime.utcnow() + product.save() + return product + + # remove product from category + def remove_from_category(self, product): + product.category = None + product.updated_at = datetime.utcnow() + product.save() + return product + + # bulk create many products in case of CSV + def bulk_create(self, products_data): + created_products = [] + + for data in products_data: + product = Product( + name=data["name"], + description=data["description"], + price=data["price"], + # brand consistency is taken care in serializers and service so need to check here again + brand=data["brand"], + quantity=data["quantity"], + category=data.get("category"), + updated_at=datetime.utcnow(), + ) + product.save() + created_products.append(product) + + return created_products + + +product_repository = ProductRepository() +product_category_repository = ProductCategoryRepository() From d4540df2a8487c36d7752de30f0066b6937b0ebb Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 18:17:08 +0530 Subject: [PATCH 30/83] week4 - serializers created --- backend/python/week4/serializers.py | 87 +++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/backend/python/week4/serializers.py b/backend/python/week4/serializers.py index e69de29bb..c2517efcb 100644 --- a/backend/python/week4/serializers.py +++ b/backend/python/week4/serializers.py @@ -0,0 +1,87 @@ +from rest_framework import serializers + + +# cheking for category API +class ProductCategorySerializer(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + title = serializers.CharField(max_length=100) + description = serializers.CharField( + requiered=False, allow_blank=True, default="", max_length=300) + # output fields only + created_at = serializers.DateTimeField(read_only=True) + updated_at = serializers.DateTimeField(read_only=True) + + # check whether title is not empty + def validate_title(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Title cannot be empty.") + return value + + +# checking for products API +class ProductSerializer(serializers.Serializer): + id = serializers.IntegerField(read_only=True) + name = serializers.CharField(max_length=200) + description = serializers.CharField() + price = serializers.DecimalField(max_digits=10, decimal_places=2) + # here brand is made compulsary not optional + brand = serializers.CharField(max_length=100) + quantity = serializers.IntegerField() + # client need not send whole data just say attach it to category number 1 + category_id = serializers.IntegerField(required=False, allow_null=True) + # category is nested output field +# { +# "id": 1, +# "name": "Rice", +# "brand": "India Gate", +# "category_id": 2, +# "category": { this is from nested giving better output +# "id": 2, +# "title": "Food", +# "description": "Daily grocery items" +# } +# } + category = ProductCategorySerializer(read_only=True) + created_at = serializers.DateTimeField(read_only=True) + updated_at = serializers.DateTimeField(read_only=True) + + # custom serialization + def validate_name(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Name cannot be empty.") + return value + + def validate_description(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Description cannot be empty.") + return value + + def validate_price(self, value): + if value <= 0: + raise serializers.ValidationError("Price must be greater than 0.") + return value + + def validate_quantity(self, value): + if value < 0: + raise serializers.ValidationError("Quantity cannot be negative.") + return value + + def validate_brand(self, value): + value = value.strip() + if not value: + raise serializers.ValidationError("Brand is required.") + return value + + +# for API like add product to category or remove from category +class ProductCategoryActionSerializer(serializers.Serializer): + product_id = serializers.IntegerField() + + def validate_product_id(self, value): + if value <= 0: + raise serializers.ValidationError( + "product_id must be a positive integer.") + return value From 430e6512fabeb58606a66d7d1de6bd613a9bbb48 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 18:18:21 +0530 Subject: [PATCH 31/83] week4 - serializers created --- backend/python/week4/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/python/week4/serializers.py b/backend/python/week4/serializers.py index c2517efcb..9e42c65c1 100644 --- a/backend/python/week4/serializers.py +++ b/backend/python/week4/serializers.py @@ -6,7 +6,7 @@ class ProductCategorySerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True) title = serializers.CharField(max_length=100) description = serializers.CharField( - requiered=False, allow_blank=True, default="", max_length=300) + required=False, allow_blank=True, default="", max_length=300) # output fields only created_at = serializers.DateTimeField(read_only=True) updated_at = serializers.DateTimeField(read_only=True) From abc449fbb62388006d551ea8af413e7b95476221 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 19:37:29 +0530 Subject: [PATCH 32/83] week4 - service updated --- backend/python/week4/service.py | 78 +++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/backend/python/week4/service.py b/backend/python/week4/service.py index e69de29bb..65b328a80 100644 --- a/backend/python/week4/service.py +++ b/backend/python/week4/service.py @@ -0,0 +1,78 @@ +# bridge between views and repository +from .repository import product_category_repository, product_repository +from .csv_helper import read_csv_file + + +class ProductCategoryService: + # creating category + def create(self, data): + existing_category = product_category_repository.get_by_title( + data["title"]) + if existing_category: + return {"error": "Category with this title already exists"} + + return product_category_repository.create(data) + + # get by id + def get(self, id): + return product_category_repository.get(id) + + # list all products + def list_all(self, filters=None, sort_by=None): + return product_category_repository.list_all(filters=filters, sort_by=sort_by) + + # update + def update(self, id, data): + category = product_category_repository.get(id) + if not category: + return {"error": "Category not found"} + existing_category = product_category_repository.get_by_title( + data["title"]) + if existing_category and existing_category.id != id: + return {"error": "Another category with this title already exists"} + else: + return product_category_repository.update(id, data) + + # delete + def delete(self, id): + category = product_category_repository.get(id) + if not category: + return {"error": "Category not found"} + product_in_category = product_repository.list_by_category(category) + if product_in_category.count() > 0: + return {"error": "Cannot delete category because products are assigned to it"} + else: + return product_category_repository.delete(id) + + # get products with category_id + def get_products(self, category_id, filters=None, sort_by=None): + category = product_category_repository.get(category_id) + if not category: + return {"error": "Category not found"} + else: + return product_repository.list_by_category(category, filters=filters, sort_by=sort_by) + + # add products to a category + def add_product_to_category(self, category_id, product_id): + category = product_category_repository.get(category_id) + if not category: + return {"error": "Category not found"} + else: + product = product_repository.get(product_id) + if not product: + return {"error": "Product not found"} + else: + return product_repository.assign_to_category(product, category) + + # remove product from category + def remove_product_category(self, category_id, product_id): + category = product_category_repository.get(category_id) + if not category: + return {"error": "Category not found"} + else: + product = product_repository.get(product_id) + if not product: + return {"error": "Product not found"} + if not product.category or product.category.id != category.id: + return {"error": "Product does not belong to this category."} + return product_repository.remove_from_category(product) From 61a7c9ce622f16203f2e81842cf002fddb411d9a Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 20:28:23 +0530 Subject: [PATCH 33/83] week4 - service updated --- backend/python/week4/service.py | 69 +++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/backend/python/week4/service.py b/backend/python/week4/service.py index 65b328a80..a4ee7ac65 100644 --- a/backend/python/week4/service.py +++ b/backend/python/week4/service.py @@ -1,8 +1,17 @@ -# bridge between views and repository +# bridge between views and repository ( business logic layer ) from .repository import product_category_repository, product_repository from .csv_helper import read_csv_file +# some rules used +# 1. brand is compulsary now +# 2. if category is missing use Miscellaneous +# 3. category title should be unique +# 4. category cannot be deleted if products belong to it +# 5. CSV rows should be validated before insertion + + +# category related business logic class ProductCategoryService: # creating category def create(self, data): @@ -13,19 +22,20 @@ def create(self, data): return product_category_repository.create(data) - # get by id + # get category by id def get(self, id): return product_category_repository.get(id) - # list all products + # list all categories def list_all(self, filters=None, sort_by=None): return product_category_repository.list_all(filters=filters, sort_by=sort_by) - # update + # update category def update(self, id, data): category = product_category_repository.get(id) if not category: return {"error": "Category not found"} + # avoid duplicating existing_category = product_category_repository.get_by_title( data["title"]) if existing_category and existing_category.id != id: @@ -33,7 +43,7 @@ def update(self, id, data): else: return product_category_repository.update(id, data) - # delete + # delete category def delete(self, id): category = product_category_repository.get(id) if not category: @@ -44,7 +54,7 @@ def delete(self, id): else: return product_category_repository.delete(id) - # get products with category_id + # get products belonging to same category_id def get_products(self, category_id, filters=None, sort_by=None): category = product_category_repository.get(category_id) if not category: @@ -74,5 +84,50 @@ def remove_product_category(self, category_id, product_id): if not product: return {"error": "Product not found"} if not product.category or product.category.id != category.id: - return {"error": "Product does not belong to this category."} + return {"error": "Product does not belong to this category"} return product_repository.remove_from_category(product) + + +# product related business logic +class ProductService: + # creating Miscellaneous category for "no category" products + def get_default_category(self): + default_category = product_category_repository.get_by_title( + "Miscellaneous") + if not default_category: + default_category = product_category_repository.create({ + "title": "Miscellaneous", + "description": "Default category for uncategorized products" + }) + return default_category + + # creating products + def create(self, data): + # brand is compulsary now + if not data.get("brand") or not str(data["brand"]).strip(): + return {"error": "Brand is required"} + category_id = data.get("category_id") + if category_id is not None: + category_obj = product_category_repository.get(category_id) + if not category_obj: + return {"error": "Category not found"} + else: + # if category is not defined then go to misc + category_obj = self.get_default_category() + product_data = { + "name": data["name"], + "description": data["description"], + "price": data["price"], + "brand": data["brand"].strip(), + "quantity": data["quantity"], + "category": category_obj, + } + return product_repository.create(product_data) + + # get product by id + def get(self, id): + return product_repository.get(id) + + # list all products + def list_all(self, filters=None, sort_by=None): + return product_repository.list_all(filters=filters, sort_by=sort_by) From 269362359832c26b833b1fa626aa709a4de7c013 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 23:25:00 +0530 Subject: [PATCH 34/83] week4 - service completed --- backend/python/week4/service.py | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/backend/python/week4/service.py b/backend/python/week4/service.py index a4ee7ac65..2aef00281 100644 --- a/backend/python/week4/service.py +++ b/backend/python/week4/service.py @@ -131,3 +131,103 @@ def get(self, id): # list all products def list_all(self, filters=None, sort_by=None): return product_repository.list_all(filters=filters, sort_by=sort_by) + + # update product + def update(self, id, data): + existing_product = product_repository.get(id) + if not existing_product: + return None + if not data.get("brand") or not str(data["brand"]).strip(): + return {"error": "Brand is required"} + category_id = data.get("category_id") + if category_id is not None: + category_obj = product_category_repository.get(category_id) + if not category_obj: + return {"error": "Category not found"} + else: + category_obj = self.get_default_category() + + product_data = { + "name": data["name"], + "description": data["description"], + "price": data["price"], + "brand": data["brand"].strip(), + "quantity": data["quantity"], + "category": category_obj + } + return product_repository.update(id, product_data) + + # delete product + def delete(self, id): + return product_repository.delete(id) + + def create_from_csv(self, uploaded_file): + # these handles must exist in CSV + required_col = ["name", "description", "price", + "brand", "quantity", "category_id"] + try: + reader = read_csv_file( + uploaded_file, requiered_columns=required_col) + except ValueError as error: + return {"error": str(error)} + + products_to_create = [] + + # start from 2 since row 1 is header line + for row_number, row in enumerate(reader, start=2): + name = (row.get("name") or "").strip() + description = (row.get("description") or "").strip() + price = (row.get("price") or "").strip() + brand = (row.get("brand") or "").strip() + quantity = (row.get("quantity") or "").strip() + category_id = (row.get("category_id") or "").strip() + + if not name: + return {"error": f"Row {row_number}: name cannot be empty"} + + if not description: + return {"error": f"Row {row_number}: description cannot be empty"} + + try: + # converting string to numbers + price = float(price) + except: + return {"error": f"Row {row_number}: invalid price"} + + if price <= 0: + return {"error": f"Row {row_number}: price must be greater than 0"} + + try: + quantity = int(quantity) + except ValueError: + return {"error": f"Row {row_number}: invalid quantity"} + + if quantity < 0: + return {"error": f"Row {row_number}: quantity cannot be negative"} + + if category_id: + try: + category_id = int(category_id) + except ValueError: + return {"error": f"Row {row_number}: invalid category_id"} + + category_obj = product_category_repository.get(category_id) + if not category_obj: + return {"error": f"Row {row_number}: category not found"} + else: + category_obj = self.get_default_category() + + products_to_create.append({ + "name": name, + "description": description, + "price": price, + "brand": brand, + "quantity": quantity, + "category": category_obj, + }) + + return product_repository.bulk_create(products_to_create) + + +product_service = ProductService() +product_category_service = ProductCategoryService() From a015fa50878fe1b416e7fd7e89f33ccad951736b Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 23:34:00 +0530 Subject: [PATCH 35/83] week4 - service completed --- backend/python/week4/service.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/python/week4/service.py b/backend/python/week4/service.py index 2aef00281..77fdef759 100644 --- a/backend/python/week4/service.py +++ b/backend/python/week4/service.py @@ -188,10 +188,13 @@ def create_from_csv(self, uploaded_file): if not description: return {"error": f"Row {row_number}: description cannot be empty"} + if not brand: + # brand is compulsary now + return {"error": f"Row {row_number}: brand is required"} + try: - # converting string to numbers price = float(price) - except: + except ValueError: return {"error": f"Row {row_number}: invalid price"} if price <= 0: From 01d0d1e29e292cf4cfd8e29f950b1d689c320086 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sun, 22 Mar 2026 23:46:57 +0530 Subject: [PATCH 36/83] week4 - pagination completed --- backend/python/week4/pagination.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 backend/python/week4/pagination.py diff --git a/backend/python/week4/pagination.py b/backend/python/week4/pagination.py new file mode 100644 index 000000000..059dd27d9 --- /dev/null +++ b/backend/python/week4/pagination.py @@ -0,0 +1,7 @@ +from rest_framework.pagination import PageNumberPagination + + +class Week4Pagination(PageNumberPagination): + page_size = 5 + page_size_query_param = "page_size" + max_page_size = 100 From 9d23472183cb76ffc605ab3edc6774a2447140d4 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 23 Mar 2026 02:50:15 +0530 Subject: [PATCH 37/83] week4 - views completed --- backend/python/week4/views.py | 337 +++++++++++++++++++++++++++++++++- 1 file changed, 335 insertions(+), 2 deletions(-) diff --git a/backend/python/week4/views.py b/backend/python/week4/views.py index 91ea44a21..37fcac0c6 100644 --- a/backend/python/week4/views.py +++ b/backend/python/week4/views.py @@ -1,3 +1,336 @@ -from django.shortcuts import render +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.parsers import MultiPartParser, FormParser +from .serializers import ProductCategoryActionSerializer, ProductCategorySerializer, ProductSerializer +from .service import product_category_service, product_service +from .repository import product_category_repository +from .pagination import Week4Pagination -# Create your views here. + +class ProductListCreateAPIView(APIView): + # listing all products with sorting, filtering and pagination + def get(self, request): + sort_by = request.query_params.get("sort_by", "id") + + filters = {} + + name = request.query_params.get("name") + brand = request.query_params.get("brand") + min_price = request.query_params.get("min_price") + max_price = request.query_params.get("max_price") + min_quantity = request.query_params.get("min_quantity") + max_quantity = request.query_params.get("max_quantity") + category_id = request.query_params.get("category_id") + + if name: + filters["name"] = name + if brand: + filters["brand"] = brand + + if min_price is not None: + try: + filters["min_price"] = float(min_price) + except ValueError: + return Response({"error": "min_price must be a number"}, status=status.HTTP_400_BAD_REQUEST) + + if max_price is not None: + try: + filters["max_price"] = float(max_price) + except ValueError: + return Response({"error": "max_price must be a number"}, status=status.HTTP_400_BAD_REQUEST) + + if min_quantity is not None: + try: + filters["min_quantity"] = int(min_quantity) + except ValueError: + return Response({"error": "min_quantity must be an integer."}, status=status.HTTP_400_BAD_REQUEST) + + if max_quantity is not None: + try: + filters["max_quantity"] = int(max_quantity) + except ValueError: + return Response({"error": "max_quantity must be an integer."}, status=status.HTTP_400_BAD_REQUEST) + + if category_id is not None: + try: + category_id = int(category_id) + except ValueError: + return Response({"error": "category_id must be an integer."}, status=status.HTTP_400_BAD_REQUEST) + + category = product_category_repository.get(category_id) + if not category: + return Response({"error": "Category not found."}, status=status.HTTP_404_NOT_FOUND) + + filters["category"] = category + + products = product_service.list_all(filters=filters, sort_by=sort_by) + paginator = Week4Pagination() + paginated_products = paginator.paginate_queryset( + products, request, view=self) + + product_data = [] + for product in paginated_products: + product_data.append(product.to_dict()) + + serializer = ProductSerializer(product_data, many=True) + return paginator.get_paginated_response(serializer.data) + + # create one product + def post(self, request): + serializer = ProductSerializer(data=request.data) + if serializer.is_valid(): + product = product_service.create(serializer.validated_data) + # we have to use isinstance bcz service now returns error instead of true/false like in week2/3 + if isinstance(product, dict) and "error" in product: + return Response(product, status=status.HTTP_400_BAD_REQUEST) + response_serializer = ProductSerializer(product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ProductDetailAPIView(APIView): + # get product with id + def get(self, request, id): + product = product_service.get(id) + if not product: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = ProductSerializer(product.to_dict()) + return Response(serializer.data, status=status.HTTP_200_OK) + + # update product + def put(self, request, id): + existing_product = product_service.get(id) + if not existing_product: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = ProductSerializer(data=request.data) + if serializer.is_valid(): + updated_product = product_service.update( + id, serializer.validated_data) + + if isinstance(updated_product, dict) and "error" in updated_product: + return Response(updated_product, status=status.HTTP_400_BAD_REQUEST) + + response_serializer = ProductSerializer(updated_product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_200_OK) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # delete product + def delete(self, request, id): + deleted = product_service.delete(id) + if not deleted: + return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND) + else: + return Response(status=status.HTTP_204_NO_CONTENT) + + +class ProductCategoryListCreateAPIView(APIView): + # list all category with pagination, sorting and filtering + def get(self, request): + sort_by = request.query_params.get("sort_by", "id") + filters = {} + title = request.query_params.get("title") + + if title: + filters["title"] = title + + categories = product_category_service.list_all( + filters=filters, sort_by=sort_by) + + paginator = Week4Pagination() + paginated_categories = paginator.paginate_queryset( + categories, request, view=self) + + category_data = [] + for category in paginated_categories: + category_data.append(category.to_dict()) + + serializer = ProductCategorySerializer(category_data, many=True) + return paginator.get_paginated_response(serializer.data) + + # create one category + def post(self, request): + serializer = ProductCategorySerializer(data=request.data) + if serializer.is_valid(): + category = product_category_service.create( + serializer.validated_data) + if isinstance(category, dict) and "error" in category: + return Response(category, status=status.HTTP_400_BAD_REQUEST) + + response_serializer = ProductCategorySerializer(category.to_dict()) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class ProductCategoryDetailAPIView(APIView): + # get one category + def get(self, request, id): + category = product_category_service.get(id) + if not category: + return Response({"error": "Category not found"}, status=status.HTTP_404_NOT_FOUND) + else: + serializer = ProductCategorySerializer(category.to_dict()) + return Response(serializer.data, status=status.HTTP_200_OK) + + # update category + def put(self, request, id): + existing_category = product_category_service.get(id) + if not existing_category: + return Response({"error": "Category not found"}, status=status.HTTP_404_NOT_FOUND) + serializer = ProductCategorySerializer(data=request.data) + if serializer.is_valid(): + updated_category = product_category_service.update( + id, serializer.validated_data) + if isinstance(updated_category, dict) and "error" in updated_category: + return Response(updated_category, status=status.HTTP_400_BAD_REQUEST) + + response_serializer = ProductCategorySerializer( + updated_category.to_dict()) + return Response(response_serializer.data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + # delete category + + def delete(self, request, id): + deleted = product_category_service.delete(id) + if isinstance(deleted, dict) and "error" in deleted: + if "Category not found" in deleted["error"]: + return Response(deleted, status=status.HTTP_404_NOT_FOUND) + else: + return Response(deleted, status=status.HTTP_400_BAD_REQUEST) + else: + return Response(status=status.HTTP_204_NO_CONTENT) + + +# list all products of one category with filter + sort + paginate +class CategoryProductsAPIView(APIView): + def get(self, request, category_id): + sort_by = request.query_params.get("sort_by", "id") + + filters = {} + + name = request.query_params.get("name") + brand = request.query_params.get("brand") + min_price = request.query_params.get("min_price") + max_price = request.query_params.get("max_price") + min_quantity = request.query_params.get("min_quantity") + max_quantity = request.query_params.get("max_quantity") + + if name: + filters["name"] = name + + if brand: + filters["brand"] = brand + + if min_price is not None: + try: + filters["min_price"] = float(min_price) + except ValueError: + return Response({"error": "min_price must be a number."}, status=status.HTTP_400_BAD_REQUEST) + + if max_price is not None: + try: + filters["max_price"] = float(max_price) + except ValueError: + return Response({"error": "max_price must be a number."}, status=status.HTTP_400_BAD_REQUEST) + + if min_quantity is not None: + try: + filters["min_quantity"] = int(min_quantity) + except ValueError: + return Response({"error": "min_quantity must be an integer."}, status=status.HTTP_400_BAD_REQUEST) + + if max_quantity is not None: + try: + filters["max_quantity"] = int(max_quantity) + except ValueError: + return Response({"error": "max_quantity must be an integer."}, status=status.HTTP_400_BAD_REQUEST) + + products = product_category_service.get_products( + category_id, + filters=filters, + sort_by=sort_by, + ) + + if isinstance(products, dict) and "error" in products: + return Response(products, status=status.HTTP_404_NOT_FOUND) + + paginator = Week4Pagination() + paginated_products = paginator.paginate_queryset( + products, request, view=self) + + product_data = [] + for product in paginated_products: + product_data.append(product.to_dict()) + + serializer = ProductSerializer(product_data, many=True) + return paginator.get_paginated_response(serializer.data) + + +# add product to a category +class AddProductToCategoryAPIView(APIView): + def post(self, request, category_id): + serializer = ProductCategoryActionSerializer(data=request.data) + + if serializer.is_valid(): + product = product_category_service.add_product_to_category( + category_id, serializer.validated_data["product_id"]) + if isinstance(product, dict) and "error" in product: + if product["error"] in ["Category not found", "Product not found"]: + return Response(product, status=status.HTTP_404_NOT_FOUND) + else: + return Response(product, status=status.HTTP_400_BAD_REQUEST) + response_serializer = ProductSerializer(product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +# remove product from category +class RemoveProductFromCategoryAPIView(APIView): + def post(self, request, category_id): + serializer = ProductCategoryActionSerializer(data=request.data) + + if serializer.is_valid(): + product = product_category_service.remove_product_category( + category_id, serializer.validated_data["product_id"]) + + if isinstance(product, dict) and "error" in product: + if product["error"] in ["Category not found", "Product not found"]: + return Response(product, status=status.HTTP_404_NOT_FOUND) + else: + return Response(product, status=status.HTTP_400_BAD_REQUEST) + + response_serializer = ProductSerializer(product.to_dict()) + return Response(response_serializer.data, status=status.HTTP_200_OK) + else: + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +# upload csv and create many products +class BulkProductUploadAPIView(APIView): + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + uploaded_file = request.FILES.get("file") + if not uploaded_file: + return Response({"error": "CSV file is required with key 'file'."}, status=status.HTTP_400_BAD_REQUEST) + + result = product_service.create_from_csv(uploaded_file) + + if isinstance(result, dict) and "error" in result: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + + product_data = [] + for product in result: + product_data.append(product.to_dict()) + + serializer = ProductSerializer(product_data, many=True) + return Response({ + "message": f"{len(product_data)} products created successfully.", + "products": serializer.data + }, status=status.HTTP_201_CREATED) From b0ac4184490c24442d60dd17b95b733277d19d00 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 23 Mar 2026 02:58:50 +0530 Subject: [PATCH 38/83] week4 - views completed --- backend/python/week4/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/python/week4/views.py b/backend/python/week4/views.py index 37fcac0c6..17a6b1604 100644 --- a/backend/python/week4/views.py +++ b/backend/python/week4/views.py @@ -313,18 +313,22 @@ def post(self, request, category_id): # upload csv and create many products class BulkProductUploadAPIView(APIView): + # normally DRF expects JSON bodies but it is not coming in form of JSON so we need to parse it parser_classes = [MultiPartParser, FormParser] def post(self, request): + # get uploaded file first uploaded_file = request.FILES.get("file") if not uploaded_file: - return Response({"error": "CSV file is required with key 'file'."}, status=status.HTTP_400_BAD_REQUEST) + return Response({"error": "CSV file is required with key 'file'"}, status=status.HTTP_400_BAD_REQUEST) + # send file to service layer result = product_service.create_from_csv(uploaded_file) if isinstance(result, dict) and "error" in result: return Response(result, status=status.HTTP_400_BAD_REQUEST) + # if service did not return error prepare dict to upload product_data = [] for product in result: product_data.append(product.to_dict()) From ce1256dbcf20a1ce8e56a042cb9f036b0539ce89 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 23 Mar 2026 03:07:04 +0530 Subject: [PATCH 39/83] week4 - urls completed --- backend/python/week4/urls.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/backend/python/week4/urls.py b/backend/python/week4/urls.py index e69de29bb..c07e95cde 100644 --- a/backend/python/week4/urls.py +++ b/backend/python/week4/urls.py @@ -0,0 +1,26 @@ +from django.urls import path +from .views import ProductListCreateAPIView, ProductDetailAPIView, ProductCategoryDetailAPIView, ProductCategoryListCreateAPIView, CategoryProductsAPIView, AddProductToCategoryAPIView, RemoveProductFromCategoryAPIView, BulkProductUploadAPIView + + +urlpatterns = [ + # CRUD product + path("products/", ProductListCreateAPIView.as_view(), + name="product-list-create"), + path("products//", ProductDetailAPIView.as_view(), name="product-detail"), + # CRUD category + path("categories/", ProductCategoryListCreateAPIView.as_view(), + name="category-list-create"), + path("categories//", ProductCategoryDetailAPIView.as_view(), + name="category-detail"), + # products belonging to same category + path("categories//products/", + CategoryProductsAPIView.as_view(), name="category-products"), + # add or remove products from category + path("categories//add-product/", + AddProductToCategoryAPIView.as_view(), name="add-product-to-category"), + path("categories//remove-product/", + RemoveProductFromCategoryAPIView.as_view(), name="remove-product-from-category"), + # csv upload + path("products/bulk-upload/", BulkProductUploadAPIView.as_view(), + name="product-bulk-upload") +] From 5c9cd9d4519ac5523f8d7d379143cf8b8e1254d8 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 23 Mar 2026 03:11:16 +0530 Subject: [PATCH 40/83] week4 - updated --- backend/python/django_app/settings.py | 6 ++++++ backend/python/django_app/urls.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/backend/python/django_app/settings.py b/backend/python/django_app/settings.py index b1b3a4e47..7502adf69 100644 --- a/backend/python/django_app/settings.py +++ b/backend/python/django_app/settings.py @@ -41,6 +41,7 @@ "week2", "week3.apps.Week3Config", "django_filters", + "week4.apps.Week4Config" ] MIDDLEWARE = [ @@ -125,3 +126,8 @@ # https://docs.djangoproject.com/en/6.0/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "week4.pagination.Week4Pagination", + "PAGE_SIZE": 5, +} diff --git a/backend/python/django_app/urls.py b/backend/python/django_app/urls.py index a49302744..caa7e2928 100644 --- a/backend/python/django_app/urls.py +++ b/backend/python/django_app/urls.py @@ -23,4 +23,6 @@ def hello_name(request): path("", include("django_app.adapters.api.urls")), path("week2/", include("week2.urls")), path("week3/", include("week3.urls")), + path("week4/", include("week4.urls")), + ] From 0b3058c8aec9c4435e1047b58118f6af8733fc5b Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 23 Mar 2026 03:48:30 +0530 Subject: [PATCH 41/83] week4 - completed --- backend/python/week4/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/python/week4/models.py b/backend/python/week4/models.py index 858faa477..d30469b3c 100644 --- a/backend/python/week4/models.py +++ b/backend/python/week4/models.py @@ -15,7 +15,7 @@ class ProductCategory(Document): # just like week3 this tells mongoengine which mongodb collection name to use meta = { - "collection": "product_categories" + "collection": "week4_product_categories" } def to_dict(self): @@ -46,7 +46,7 @@ class Product(Document): updated_at = DateTimeField(default=datetime.utcnow) meta = { - "collection": "products" + "collection": "week4_products" } def to_dict(self): From 4e223cb6dd0099b00d57a8bf0bdc220e575caab3 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 09:44:29 +0530 Subject: [PATCH 42/83] migration script completed --- backend/python/week4/migrate_old_products.py | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 backend/python/week4/migrate_old_products.py diff --git a/backend/python/week4/migrate_old_products.py b/backend/python/week4/migrate_old_products.py new file mode 100644 index 000000000..4c946067d --- /dev/null +++ b/backend/python/week4/migrate_old_products.py @@ -0,0 +1,79 @@ +from datetime import datetime +from .db_connection import initialize_mongo +from .models import Product, ProductCategory + +DEFAULT_CATEGORY_TITLE = "Miscellaneous" +DEFAULT_CATEGORY_DESCRIPTION = "Default category for uncategorized products" +DEFAULT_BRAND = "Unknown" + + +def create_default_category(): + category = ProductCategory.objects( + title__iexact=DEFAULT_CATEGORY_TITLE).first() + if not category: + category = ProductCategory( + title=DEFAULT_CATEGORY_TITLE, + description=DEFAULT_CATEGORY_DESCRIPTION, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ) + category.save() + print(f"Created default category: {DEFAULT_CATEGORY_TITLE}") + + else: + print(f"Default category already exists: {DEFAULT_CATEGORY_TITLE}") + + return category + + +def migrate_product(): + # connect mongodb + initialize_mongo() + print("Connected to MongoDB") + + # ensuring default categories + default_category = create_default_category() + + # finding and fixing all products + tot_prod_checked = 0 + cat_fix_count = 0 + brand_fix_count = 0 + tot_prod_upd = 0 + + products = Product.objects + + for product in products: + tot_prod_checked += 1 + changed = False + + # fix missing category + if product.category is None: + product.category = default_category + cat_fix_count += 1 + changed = True + + # fix missing brand + if product.brand is not None: + current_brand = product.brand + else: + current_brand = "" + if not str(current_brand).strip(): + product.brand = DEFAULT_BRAND + brand_fix_count += 1 + changed = True + + # save only if something is changed + if changed: + product.updated_at = datetime.utcnow() + product.save() + tot_prod_upd += 1 + + print("\nMigration completed successfully") + print(f"Total products checked : {tot_prod_checked}") + print(f"Products updated : {tot_prod_upd}") + print(f"Missing category fixed : {cat_fix_count}") + print(f"Missing brand fixed : {brand_fix_count}") + + +if __name__ == "__main__": + migrate_product() From ceae03b421355c8572ca223ae99a0b3e3c6fcb4d Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 10:11:19 +0530 Subject: [PATCH 43/83] migration script completed --- backend/python/week4/migrate_old_products.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/python/week4/migrate_old_products.py b/backend/python/week4/migrate_old_products.py index 4c946067d..ae072ec0b 100644 --- a/backend/python/week4/migrate_old_products.py +++ b/backend/python/week4/migrate_old_products.py @@ -7,6 +7,7 @@ DEFAULT_BRAND = "Unknown" +# creating default category def create_default_category(): category = ProductCategory.objects( title__iexact=DEFAULT_CATEGORY_TITLE).first() From 33815cdc1833c2e7fa217cda75afe834947edf65 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 10:39:18 +0530 Subject: [PATCH 44/83] week4 - updated --- backend/python/week4/db_connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/python/week4/db_connection.py b/backend/python/week4/db_connection.py index 73df8eeab..2d9f57bb9 100644 --- a/backend/python/week4/db_connection.py +++ b/backend/python/week4/db_connection.py @@ -12,6 +12,7 @@ def initialize_mongo(): mongo_port = os.getenv("MONGO_PORT", "27019") mongo_db = os.getenv("MONGO_DB", "interneers_lab_week4") + # mongodb://root:example@localhost:27019/interneers_lab_week4?authSource=admin mongo_uri = ( f"mongodb://{mongo_user}:{mongo_pass}" f"@{mongo_host}:{mongo_port}/{mongo_db}?authSource=admin" From 039396469322bc21346a8951075bbe1fb8e06f08 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 10:42:35 +0530 Subject: [PATCH 45/83] readme updated --- backend/python/README.md | 117 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/backend/python/README.md b/backend/python/README.md index 189b97961..b7272a59f 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -316,6 +316,123 @@ Week 3 was about moving from a basic CRUD project to a realistic, layered backen --- +# Week 4 + +## API Reference + +--- + +## 1. Product APIs + +| Method | URL | Description | +| ------ | ----------------------- | ---------------------- | +| GET | `/week4/products/` | List all products | +| POST | `/week4/products/` | Create a new product | +| GET | `/week4/products//` | Get a product by ID | +| PUT | `/week4/products//` | Update a product by ID | +| DELETE | `/week4/products//` | Delete a product by ID | + +--- + +## 2. Category APIs + +| Method | URL | Description | +| ------ | ------------------------- | ----------------------- | +| GET | `/week4/categories/` | List all categories | +| POST | `/week4/categories/` | Create a new category | +| GET | `/week4/categories//` | Get a category by ID | +| PUT | `/week4/categories//` | Update a category by ID | +| DELETE | `/week4/categories//` | Delete a category by ID | + +--- + +## 3. Category–Product Relation APIs + +| Method | URL | Description | +| ------ | ---------------------------------------- | -------------------------------- | +| GET | `/week4/categories//products/` | List all products in a category | +| POST | `/week4/categories//add-product/` | Add a product to a category | +| POST | `/week4/categories//remove-product/` | Remove a product from a category | + +--- + +## 4. Bulk Upload + +| Method | URL | Description | +| ------ | ------------------------------ | ---------------------------------------- | +| POST | `/week4/products/bulk-upload/` | Upload a CSV to create multiple products | + +--- + +## 5. Filtering, Sorting & Pagination + +### Products — `/week4/products/` + +| Param | Example | Description | +| ------------------------------- | ------------------------------------- | --------------------------- | +| `name` | `?name=rice` | Filter by name | +| `brand` | `?brand=dove` | Filter by brand | +| `min_price` / `max_price` | `?min_price=20&max_price=200` | Filter by price range | +| `min_quantity` / `max_quantity` | `?min_quantity=5&max_quantity=50` | Filter by quantity range | +| `category_id` | `?category_id=1` | Filter by category | +| `sort_by` | `?sort_by=price` or `?sort_by=-price` | Sort ascending / descending | +| `page` / `page_size` | `?page=1&page_size=5` | Paginate results | + +### Categories — `/week4/categories/` + +| Param | Example | Description | +| -------------------- | ---------------------- | --------------------------- | +| `title` | `?title=food` | Filter by title | +| `sort_by` | `?sort_by=-created_at` | Sort ascending / descending | +| `page` / `page_size` | `?page=1&page_size=5` | Paginate results | + +--- + +## 6. Key Business Rules + +- `brand` is **required** for all product operations +- Products without a `category_id` are assigned to **Miscellaneous** +- Category **titles must be unique** +- A category **cannot be deleted** if products are assigned to it +- CSV bulk upload **validates all rows** before creating any product + +--- + +## 7. Legacy Product Migration + +A one-time migration script is included to update older product records created before category and strict brand handling were introduced. + +The script does the following: + +- Connects to MongoDB +- Checks whether the default category `Miscellaneous` exists, and creates it if needed +- Finds legacy products with missing category +- Finds legacy products with missing or blank brand +- Updates only the affected records +- Prints a summary of how many products were checked and fixed + +### How to run + +Start the services first: + +```bash +docker compose up -d +``` + +Then run the migration script: + +```bash +python -m week4.migrate_old_products +``` + +### Notes + +- This is a manual one-time migration script. +- It is mainly intended for old records created before the Week 4 category changes. +- Running it again is safe because already fixed products will not be modified again. + +--- + # Interneers Lab - Backend in Python Welcome to the **Interneers Lab 2026** Python backend! This serves as a minimal starter kit for learning and experimenting with: From d96527d33ab2a3c1348ad78e846c7a57b2d68f38 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 10:52:54 +0530 Subject: [PATCH 46/83] readme updated --- backend/python/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/backend/python/README.md b/backend/python/README.md index b7272a59f..c7c2f7965 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -433,6 +433,15 @@ python -m week4.migrate_old_products --- +## 8. Bulk CSV Upload via Postman + +1. **Prepare CSV** — Create a `.csv` file with columns: `name, description, price, brand, quantity, category_id`. Leave `category_id` blank to assign to _Miscellaneous_. +2. **Set request** — Method: `POST`, URL: `http://127.0.0.1:8000/week4/products/bulk-upload/` +3. **Set body** — Go to `Body → form-data`, add a key named `file`, change type to `File`, and select your CSV. +4. **Send** — Click Send. All rows are validated before any product is created. + +--- + # Interneers Lab - Backend in Python Welcome to the **Interneers Lab 2026** Python backend! This serves as a minimal starter kit for learning and experimenting with: From 4a19e05371d6e87aa91ae8dfbb496cea86d53f35 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 11:52:34 +0530 Subject: [PATCH 47/83] week4 - seeding added --- backend/python/README.md | 14 ++--- backend/python/week4/apps.py | 2 + backend/python/week4/seed.py | 99 ++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 backend/python/week4/seed.py diff --git a/backend/python/README.md b/backend/python/README.md index c7c2f7965..26008f861 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -300,13 +300,13 @@ Application is ready for Product CRUD with MongoDB persistence ## Example Endpoints -| Method | Endpoint | Description | -| ------------- | ----------------------- | ------------------ | -| `POST` | `/week3/products/` | Create product | -| `GET` | `/week3/products/` | Fetch all products | -| `GET` | `/week3/products//` | Fetch one product | -| `PUT`/`PATCH` | `/week3/products//` | Update product | -| `DELETE` | `/week3/products//` | Delete product | +| Method | Endpoint | Description | +| -------- | ----------------------- | ------------------ | +| `POST` | `/week3/products/` | Create product | +| `GET` | `/week3/products/` | Fetch all products | +| `GET` | `/week3/products//` | Fetch one product | +| `PUT` | `/week3/products//` | Update product | +| `DELETE` | `/week3/products//` | Delete product | --- diff --git a/backend/python/week4/apps.py b/backend/python/week4/apps.py index 14430f577..0b701ab78 100644 --- a/backend/python/week4/apps.py +++ b/backend/python/week4/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig from .db_connection import initialize_mongo +from .seed import startup_seed_and_migration class Week4Config(AppConfig): @@ -7,3 +8,4 @@ class Week4Config(AppConfig): def ready(self): initialize_mongo() + startup_seed_and_migration() diff --git a/backend/python/week4/seed.py b/backend/python/week4/seed.py new file mode 100644 index 000000000..ac0aaf5a9 --- /dev/null +++ b/backend/python/week4/seed.py @@ -0,0 +1,99 @@ +from datetime import datetime +from .models import Product, ProductCategory + +DEFAULT_CATEGORY_TITLE = "Miscellaneous" +DEFAULT_CATEGORY_DESCRIPTION = "Default category for uncategorized products" +DEFAULT_BRAND = "Unknown" + + +# ensures default category exists, it is also safe to run multiple time (idempotency) +def seed_prod_category(): + created_count = 0 + existing = ProductCategory.objects( + title__iexact=DEFAULT_CATEGORY_TITLE + ).first() + + if not existing: + ProductCategory( + title=DEFAULT_CATEGORY_TITLE, + description=DEFAULT_CATEGORY_DESCRIPTION, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ).save() + created_count += 1 + + return created_count + + +# returns the miscellaneos categorym creats if it doesnt exist +def get_default_category(): + category = ProductCategory.objects( + title__iexact=DEFAULT_CATEGORY_TITLE + ).first() + + if not category: + category = ProductCategory( + title=DEFAULT_CATEGORY_TITLE, + description=DEFAULT_CATEGORY_DESCRIPTION, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow(), + ) + category.save() + + return category + + +# fixing old products +def migrate_products(): + default_category = get_default_category() + tot_prod_check = 0 + cat_fix_count = 0 + brand_fix_count = 0 + tot_prod_upd = 0 + + products = Product.objects + + for product in products: + tot_prod_check += 1 + changed = False + + # fix missing category + if product.category is None: + product.category = default_category + cat_fix_count += 1 + changed = True + + # fix missing brand + current_brand = product.brand if product.brand is not None else "" + if not str(current_brand).strip(): + product.brand = DEFAULT_BRAND + brand_fix_count += 1 + changed = True + + if changed: + product.updated_at = datetime.utcnow() + product.save() + tot_prod_upd += 1 + + return { + "tot_prod_check": tot_prod_check, + "cat_fix_count": cat_fix_count, + "brand_fix_count": brand_fix_count, + "tot_prod_upd": tot_prod_upd, + } + + +def startup_seed_and_migration(): + categories_created = seed_prod_category() + migration_summary = migrate_products() + + print("\nStartup seed/migration summary") + print(f"Categories created : {categories_created}") + print( + f"Products checked : {migration_summary['tot_prod_check']}") + print( + f"Products updated : {migration_summary['tot_prod_upd']}") + print( + f"Category fixes applied : {migration_summary['cat_fix_count']}") + print( + f"Brand fixes applied : {migration_summary['brand_fix_count']}") From 09e11fff31eac260b4baccbc8a08d4af66786cf0 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 11:56:21 +0530 Subject: [PATCH 48/83] readme updated --- backend/python/README.md | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index 26008f861..975b7ab47 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -398,7 +398,7 @@ Week 3 was about moving from a basic CRUD project to a realistic, layered backen --- -## 7. Legacy Product Migration +## 7. Old Product Migration A one-time migration script is included to update older product records created before category and strict brand handling were introduced. @@ -406,8 +406,8 @@ The script does the following: - Connects to MongoDB - Checks whether the default category `Miscellaneous` exists, and creates it if needed -- Finds legacy products with missing category -- Finds legacy products with missing or blank brand +- Finds old products with missing category +- Finds old products with missing or blank brand - Updates only the affected records - Prints a summary of how many products were checked and fixed @@ -433,7 +433,38 @@ python -m week4.migrate_old_products --- -## 8. Bulk CSV Upload via Postman +## 8. Startup Seeding & Auto Migration + +On every Django server startup, the app automatically seeds and migrates the database via `AppConfig.ready()`: + +- Ensures the default category **Miscellaneous** exists +- Assigns `Miscellaneous` to products with a missing category +- Assigns `"Unknown"` to products with a missing or blank brand + +This runs automatically — no manual step needed. The process is **idempotent**: already-fixed records are never modified again. + +### Startup output example + +``` +Startup seed/migration summary +Categories created : 1 +Products checked : 12 +Products updated : 4 +Category fixes applied : 4 +Brand fixes applied : 2 +``` + +### Files involved + +| File | Purpose | +| ------------------ | ------------------------------------ | +| `apps.py` | Triggers startup logic via `ready()` | +| `seed.py` | Contains seeding and migration logic | +| `db_connection.py` | Handles MongoDB connection | + +--- + +## 9. Bulk CSV Upload via Postman 1. **Prepare CSV** — Create a `.csv` file with columns: `name, description, price, brand, quantity, category_id`. Leave `category_id` blank to assign to _Miscellaneous_. 2. **Set request** — Method: `POST`, URL: `http://127.0.0.1:8000/week4/products/bulk-upload/` From c32479d76d9b2a721efebee478313e50f1ad1b42 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Sat, 28 Mar 2026 11:57:35 +0530 Subject: [PATCH 49/83] readme updated --- backend/python/README.md | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index 975b7ab47..bdc52334b 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -441,26 +441,7 @@ On every Django server startup, the app automatically seeds and migrates the dat - Assigns `Miscellaneous` to products with a missing category - Assigns `"Unknown"` to products with a missing or blank brand -This runs automatically — no manual step needed. The process is **idempotent**: already-fixed records are never modified again. - -### Startup output example - -``` -Startup seed/migration summary -Categories created : 1 -Products checked : 12 -Products updated : 4 -Category fixes applied : 4 -Brand fixes applied : 2 -``` - -### Files involved - -| File | Purpose | -| ------------------ | ------------------------------------ | -| `apps.py` | Triggers startup logic via `ready()` | -| `seed.py` | Contains seeding and migration logic | -| `db_connection.py` | Handles MongoDB connection | +This runs automatically — no manual step needed. The process is idempotent: already-fixed records are never modified again. --- From d36ceb329b7d796da1d813007f64ad1930e104c7 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Mon, 30 Mar 2026 23:43:25 +0530 Subject: [PATCH 50/83] week5 - started --- backend/python/week5/__init__.py | 0 backend/python/week5/dashboard.py | 93 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 backend/python/week5/__init__.py create mode 100644 backend/python/week5/dashboard.py diff --git a/backend/python/week5/__init__.py b/backend/python/week5/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week5/dashboard.py b/backend/python/week5/dashboard.py new file mode 100644 index 000000000..d7d8a8770 --- /dev/null +++ b/backend/python/week5/dashboard.py @@ -0,0 +1,93 @@ +import streamlit as st +import pandas as pd +from week4.db_connection import initialize_mongo +from week4.models import Product, ProductCategory +from decimal import Decimal + + +initialize_mongo() + + +# page settings +st.set_page_config(page_title="Week 5 Inventory Dashboard", layout="wide") +st.title("Week 5: Interactive Data Tools") +st.write("Streamlit dashboard connected directly to MongoDB using MongoEngine") + + +# helpers +def get_all_categories(): + return list(ProductCategory.objects().order_by("title")) + + +def fetch_products(selected_category_id=None): + if selected_category_id is None: + products = Product.objects() + else: + products = Product.objects(category=selected_category_id) + + rows = [] + + for product in products: + rows.append({ + "ID": product.id, + "Name": product.name, + "Description": product.description, + "Price": product.price, + "Brand": product.brand, + "Quantity": product.quantity, + "Category": product.category.title + }) + + return pd.DataFrame(rows) + + +# creating sidebar category filter +st.sidebar.header("Filter Inventory") + +all_category = get_all_categories() +# creating dictionary where all maps to none so that no conflict arises +category_options = {"All": None} + +for category in all_category: + category_options[category.title] = category.id + +selected_category_title = st.sidebar.selectbox( + "Select Product Category", list(category_options.keys())) + +selected_category_id = category_options[selected_category_title] + + +# showing inventory table for current id +@st.fragment +def inventory_table(category_id): + st.subheader("Current Inventory") + df = fetch_products(category_id) + if df.empty: + st.info("No products found in this category") + else: + st.dataframe(df, use_container_width=True) + return df + + +# showing stock alert +@st.fragment +def stock_alert(df): + st.subheader("Stock Alert") + if not df.empty: + low_stock_row = [] + for index, row in df.iterrows(): + if row["Quantity"] < 5: + low_stock_row.append(row) + low_stock_df = pd.DataFrame(low_stock_row) + + if not low_stock_df.empty: + st.error("Some products are running low in stock") + st.dataframe(low_stock_df, use_container_width=True) + else: + st.success("No products are running low in stock") + else: + st.info("No products found in this category") + + +df = inventory_table(selected_category_id) +stock_alert(df) From 40ea1c66b70c479cced9d3e425f8283b216eaf89 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 00:32:06 +0530 Subject: [PATCH 51/83] week5 - dashboard created --- backend/python/week5/dashboard.py | 76 +++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/backend/python/week5/dashboard.py b/backend/python/week5/dashboard.py index d7d8a8770..9d0a75dbd 100644 --- a/backend/python/week5/dashboard.py +++ b/backend/python/week5/dashboard.py @@ -35,7 +35,7 @@ def fetch_products(selected_category_id=None): "Price": product.price, "Brand": product.brand, "Quantity": product.quantity, - "Category": product.category.title + "Category": product.category.title if product.category else "No Category" }) return pd.DataFrame(rows) @@ -71,14 +71,20 @@ def inventory_table(category_id): # showing stock alert @st.fragment +def stock_alert(df): + + def stock_alert(df): st.subheader("Stock Alert") + if not df.empty: - low_stock_row = [] + low_stock_rows = [] + for index, row in df.iterrows(): if row["Quantity"] < 5: - low_stock_row.append(row) - low_stock_df = pd.DataFrame(low_stock_row) + low_stock_rows.append(row) + + low_stock_df = pd.DataFrame(low_stock_rows) if not low_stock_df.empty: st.error("Some products are running low in stock") @@ -89,5 +95,67 @@ def stock_alert(df): st.info("No products found in this category") +# adding product +@st.fragment +def add_product(categories): + st.subheader("Add Product") + category_title = [] + for category in categories: + category_title.append(category.title) + + with st.form("add_product_form"): + new_name = st.text_input("Name") + new_description = st.text_area("Description") + new_price = st.number_input( + "Price", min_value=0.0, step=1.0, format="%.2f") + new_brand = st.text_input("Brand") + new_quantity = st.number_input( + "Quantity", min_value=0, step=1) + new_category_title = st.selectbox("Category", category_title if category_title else [ + "No categories found "]) + submitted = st.form_submit_button("Add Product") + + if submitted: + if not new_name.strip(): + st.error("Product name cannot be empty") + elif not new_description.strip(): + st.error("Description cannot be empty") + elif not new_brand.strip(): + st.error("Brand cannot be empty") + elif not new_category_title: + st.error("No category exist. Pls create a category first in week 4") + else: + selected_category = ProductCategory.objects( + title=new_category_title).first() + Product(name=new_name, description=new_description, + price=Decimal(str(new_price)), brand=new_brand, quantity=int(new_quantity), category=selected_category).save() + st.success(f"Product '{new_name}' added successfully") + st.rerun() + + +# removing product +@st.fragment +def remove_product(): + st.subheader("Remove Product") + all_products = Product.objects().order_by("name") + product_options = {} + for product in all_products: + label = f"{product.name} (ID: {product.id})" + product_options[label] = product.id + + if product_options: + selected_product_label = st.selectbox( + "Select Product to remove", list(product_options.keys())) + if st.button("Remove Product"): + selected_product_id = product_options[selected_product_label] + Product.objects(id=selected_product_id).delete() + st.success("Product removed successfully") + st.rerun() + else: + st.info("No products found") + + df = inventory_table(selected_category_id) stock_alert(df) +add_product(all_category) +remove_product() From 42e2925443a46ce2f78eebd7e1216e0ae4b0167b Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 00:35:11 +0530 Subject: [PATCH 52/83] week5 - dashboard created --- backend/python/week5/dashboard.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/backend/python/week5/dashboard.py b/backend/python/week5/dashboard.py index 9d0a75dbd..a92837f00 100644 --- a/backend/python/week5/dashboard.py +++ b/backend/python/week5/dashboard.py @@ -32,7 +32,7 @@ def fetch_products(selected_category_id=None): "ID": product.id, "Name": product.name, "Description": product.description, - "Price": product.price, + "Price": float(product.price), "Brand": product.brand, "Quantity": product.quantity, "Category": product.category.title if product.category else "No Category" @@ -71,20 +71,14 @@ def inventory_table(category_id): # showing stock alert @st.fragment -def stock_alert(df): - - def stock_alert(df): st.subheader("Stock Alert") - if not df.empty: - low_stock_rows = [] - + low_stock_row = [] for index, row in df.iterrows(): if row["Quantity"] < 5: - low_stock_rows.append(row) - - low_stock_df = pd.DataFrame(low_stock_rows) + low_stock_row.append(row) + low_stock_df = pd.DataFrame(low_stock_row) if not low_stock_df.empty: st.error("Some products are running low in stock") @@ -146,11 +140,11 @@ def remove_product(): if product_options: selected_product_label = st.selectbox( "Select Product to remove", list(product_options.keys())) - if st.button("Remove Product"): - selected_product_id = product_options[selected_product_label] - Product.objects(id=selected_product_id).delete() - st.success("Product removed successfully") - st.rerun() + if st.button("Remove Product"): + selected_product_id = product_options[selected_product_label] + Product.objects(id=selected_product_id).delete() + st.success("Product removed successfully") + st.rerun() else: st.info("No products found") From 65b3e041256bbab7b75101bb5b686dbdf6397b8d Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 02:24:46 +0530 Subject: [PATCH 53/83] week5 - notebook created --- .../notebook/week5_inventory_analysis.ipynb | 839 ++++++++++++++++++ 1 file changed, 839 insertions(+) create mode 100644 backend/python/week5/notebook/week5_inventory_analysis.ipynb diff --git a/backend/python/week5/notebook/week5_inventory_analysis.ipynb b/backend/python/week5/notebook/week5_inventory_analysis.ipynb new file mode 100644 index 000000000..eb4b88a1c --- /dev/null +++ b/backend/python/week5/notebook/week5_inventory_analysis.ipynb @@ -0,0 +1,839 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "87b2d0cc", + "metadata": {}, + "source": [ + "# Week 5 Inventory Analysis " + ] + }, + { + "cell_type": "code", + "execution_count": 140, + "id": "1975c454", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Project root added: c:\\Users\\Kreesh\\OneDrive\\Desktop\\Rippling\\interneers-lab\\backend\\python\n" + ] + } + ], + "source": [ + "# since the file is week5/notebook it is unable to access week4 so adding this to access all weeks\n", + "import os\n", + "import sys\n", + "\n", + "project_root = os.path.abspath(\"../..\")\n", + "\n", + "if project_root not in sys.path:\n", + " sys.path.insert(0, project_root)\n", + "\n", + "print(\"Project root added:\", project_root)" + ] + }, + { + "cell_type": "code", + "execution_count": 141, + "id": "4175ca63", + "metadata": {}, + "outputs": [], + "source": [ + "from week4.db_connection import initialize_mongo\n", + "from week4.models import Product, ProductCategory\n", + "\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns" + ] + }, + { + "cell_type": "code", + "execution_count": 142, + "id": "c645f265", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Database connected successfully\n" + ] + } + ], + "source": [ + "initialize_mongo()\n", + "print(\"Database connected successfully\")" + ] + }, + { + "cell_type": "markdown", + "id": "8230e45e", + "metadata": {}, + "source": [ + "## Fetch All Products\n", + "\n", + "This cell runs a raw MongoEngine query to get all products directly from MongoDB." + ] + }, + { + "cell_type": "code", + "execution_count": 143, + "id": "2e158954", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total products: 12\n", + "{'id': 2, 'name': 'Soap', 'description': 'Bath soap', 'price': '50.00', 'brand': 'Dove', 'quantity': 15, 'category': {'id': 1, 'title': 'Food', 'description': 'Daily grocery items', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000)}, 'category_id': 1, 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 840000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 58, 55, 445000)}\n", + "{'id': 3, 'name': 'Rice', 'description': 'Basmati rice', 'price': '400.00', 'brand': 'India Gate', 'quantity': 10, 'category': {'id': 1, 'title': 'Food', 'description': 'Daily grocery items', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000)}, 'category_id': 1, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 402000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 402000)}\n", + "{'id': 5, 'name': 'Pan', 'description': 'Non-stick pan', 'price': '999.00', 'brand': 'Prestige', 'quantity': 5, 'category': {'id': 2, 'title': 'Electronics', 'description': 'Electronic products', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 395000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 394000)}, 'category_id': 2, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 420000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 420000)}\n", + "{'id': 6, 'name': 'Soap', 'description': 'Bath soap', 'price': '45.00', 'brand': 'Dove', 'quantity': 20, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 430000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 430000)}\n", + "{'id': ObjectId('69c76070d11850d8ae737d65'), 'name': 'Test Product 1', 'description': 'No category no brand', 'price': '100.00', 'brand': 'Unknown', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 363000)}\n", + "{'id': ObjectId('69c76078d11850d8ae737d67'), 'name': 'Test Product 2', 'description': 'No category but has brand', 'price': '200.00', 'brand': 'Nike', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 365000)}\n", + "{'id': ObjectId('69c7607ed11850d8ae737d69'), 'name': 'Test Product 3', 'description': 'Has category but no brand', 'price': '150.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 368000)}\n", + "{'id': ObjectId('69c7728dd11850d8ae737d6c'), 'name': 'Legacy Product 1', 'description': 'Missing category only', 'price': '100.00', 'brand': 'Nike', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 669000)}\n", + "{'id': ObjectId('69c77295d11850d8ae737d6e'), 'name': 'Legacy Product 2', 'description': 'Missing brand only', 'price': '120.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719613), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 673000)}\n", + "{'id': ObjectId('69c772a8d11850d8ae737d70'), 'name': 'Legacy Product 3', 'description': 'Missing both brand and category', 'price': '150.00', 'brand': 'Unknown', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719613), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 675000)}\n" + ] + } + ], + "source": [ + "products = Product.objects()\n", + "print(\"Total products:\", products.count())\n", + "for product in products[:10]:\n", + " print(product.to_dict())" + ] + }, + { + "cell_type": "markdown", + "id": "fbacba48", + "metadata": {}, + "source": [ + "## Fetch All Categories\n", + "\n", + "This cell gets all product categories from MongoDB." + ] + }, + { + "cell_type": "code", + "execution_count": 144, + "id": "bb270c30", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total categories: 4\n", + "{'id': 1, 'title': 'Food', 'description': 'Daily grocery items', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000)}\n", + "{'id': 2, 'title': 'Electronics', 'description': 'Electronic products', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 395000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 394000)}\n", + "{'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}\n", + "{'id': 4, 'title': 'Kitchen', 'description': 'Kitchen items', 'created_at': datetime.datetime(2026, 3, 22, 22, 3, 25, 203000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 3, 25, 203000)}\n" + ] + } + ], + "source": [ + "categories = ProductCategory.objects()\n", + "print(\"Total categories:\", categories.count())\n", + "for category in categories[:5]:\n", + " print(category.to_dict())" + ] + }, + { + "cell_type": "markdown", + "id": "55dad7d9", + "metadata": {}, + "source": [ + "## Convert Product Data into a DataFrame\n", + "\n", + "MongoEngine returns document objects. \n", + "This cell converts product data into tabular form so it becomes easier to analyze and visualize." + ] + }, + { + "cell_type": "code", + "execution_count": 145, + "id": "05aab583", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idnamedescriptionpricebrandquantitycategory
02SoapBath soap50.0Dove15Food
13RiceBasmati rice400.0India Gate10Food
25PanNon-stick pan999.0Prestige5Electronics
36SoapBath soap45.0Dove20Miscellaneous
469c76070d11850d8ae737d65Test Product 1No category no brand100.0Unknown5Miscellaneous
569c76078d11850d8ae737d67Test Product 2No category but has brand200.0Nike10Miscellaneous
669c7607ed11850d8ae737d69Test Product 3Has category but no brand150.0Unknown8Miscellaneous
769c7728dd11850d8ae737d6cLegacy Product 1Missing category only100.0Nike5Miscellaneous
869c77295d11850d8ae737d6eLegacy Product 2Missing brand only120.0Unknown8Miscellaneous
969c772a8d11850d8ae737d70Legacy Product 3Missing both brand and category150.0Unknown10Miscellaneous
1069c772b5d11850d8ae737d72Legacy Product 4Already correct200.0Dove12Miscellaneous
118Pancooking pan1000.0prestige4Kitchen
\n", + "
" + ], + "text/plain": [ + " id name \\\n", + "0 2 Soap \n", + "1 3 Rice \n", + "2 5 Pan \n", + "3 6 Soap \n", + "4 69c76070d11850d8ae737d65 Test Product 1 \n", + "5 69c76078d11850d8ae737d67 Test Product 2 \n", + "6 69c7607ed11850d8ae737d69 Test Product 3 \n", + "7 69c7728dd11850d8ae737d6c Legacy Product 1 \n", + "8 69c77295d11850d8ae737d6e Legacy Product 2 \n", + "9 69c772a8d11850d8ae737d70 Legacy Product 3 \n", + "10 69c772b5d11850d8ae737d72 Legacy Product 4 \n", + "11 8 Pan \n", + "\n", + " description price brand quantity \\\n", + "0 Bath soap 50.0 Dove 15 \n", + "1 Basmati rice 400.0 India Gate 10 \n", + "2 Non-stick pan 999.0 Prestige 5 \n", + "3 Bath soap 45.0 Dove 20 \n", + "4 No category no brand 100.0 Unknown 5 \n", + "5 No category but has brand 200.0 Nike 10 \n", + "6 Has category but no brand 150.0 Unknown 8 \n", + "7 Missing category only 100.0 Nike 5 \n", + "8 Missing brand only 120.0 Unknown 8 \n", + "9 Missing both brand and category 150.0 Unknown 10 \n", + "10 Already correct 200.0 Dove 12 \n", + "11 cooking pan 1000.0 prestige 4 \n", + "\n", + " category \n", + "0 Food \n", + "1 Food \n", + "2 Electronics \n", + "3 Miscellaneous \n", + "4 Miscellaneous \n", + "5 Miscellaneous \n", + "6 Miscellaneous \n", + "7 Miscellaneous \n", + "8 Miscellaneous \n", + "9 Miscellaneous \n", + "10 Miscellaneous \n", + "11 Kitchen " + ] + }, + "execution_count": 145, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "rows = []\n", + "for product in products:\n", + " rows.append({\n", + " \"id\": product.id,\n", + " \"name\": product.name,\n", + " \"description\": product.description,\n", + " \"price\": float(product.price),\n", + " \"brand\": product.brand,\n", + " \"quantity\": product.quantity,\n", + " \"category\": product.category.title if product.category else \"No Category\"\n", + " })\n", + "df = pd.DataFrame(rows)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "09aa46ed", + "metadata": {}, + "source": [ + "## Basic Data Inspection\n", + "\n", + "This helps us understand the structure of the inventory dataset." + ] + }, + { + "cell_type": "code", + "execution_count": 146, + "id": "9f321f59", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "RangeIndex: 12 entries, 0 to 11\n", + "Data columns (total 7 columns):\n", + " # Column Non-Null Count Dtype \n", + "--- ------ -------------- ----- \n", + " 0 id 12 non-null object \n", + " 1 name 12 non-null object \n", + " 2 description 12 non-null object \n", + " 3 price 12 non-null float64\n", + " 4 brand 12 non-null object \n", + " 5 quantity 12 non-null int64 \n", + " 6 category 12 non-null object \n", + "dtypes: float64(1), int64(1), object(5)\n", + "memory usage: 804.0+ bytes\n" + ] + } + ], + "source": [ + "df.info()" + ] + }, + { + "cell_type": "code", + "execution_count": 147, + "id": "df40dff6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idnamedescriptionpricebrandquantitycategory
count12.0121212.0000001212.00000012
unique12.01011NaN6NaN4
top2.0SoapBath soapNaNUnknownNaNMiscellaneous
freq1.022NaN4NaN8
meanNaNNaNNaN292.833333NaN9.333333NaN
stdNaNNaNNaN342.837049NaN4.696872NaN
minNaNNaNNaN45.000000NaN4.000000NaN
25%NaNNaNNaN100.000000NaN5.000000NaN
50%NaNNaNNaN150.000000NaN9.000000NaN
75%NaNNaNNaN250.000000NaN10.500000NaN
maxNaNNaNNaN1000.000000NaN20.000000NaN
\n", + "
" + ], + "text/plain": [ + " id name description price brand quantity category\n", + "count 12.0 12 12 12.000000 12 12.000000 12\n", + "unique 12.0 10 11 NaN 6 NaN 4\n", + "top 2.0 Soap Bath soap NaN Unknown NaN Miscellaneous\n", + "freq 1.0 2 2 NaN 4 NaN 8\n", + "mean NaN NaN NaN 292.833333 NaN 9.333333 NaN\n", + "std NaN NaN NaN 342.837049 NaN 4.696872 NaN\n", + "min NaN NaN NaN 45.000000 NaN 4.000000 NaN\n", + "25% NaN NaN NaN 100.000000 NaN 5.000000 NaN\n", + "50% NaN NaN NaN 150.000000 NaN 9.000000 NaN\n", + "75% NaN NaN NaN 250.000000 NaN 10.500000 NaN\n", + "max NaN NaN NaN 1000.000000 NaN 20.000000 NaN" + ] + }, + "execution_count": 147, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.describe(include=\"all\")" + ] + }, + { + "cell_type": "markdown", + "id": "7b842818", + "metadata": {}, + "source": [ + "## Category-wise Product Count\n", + "\n", + "This counts how many products belong to each category." + ] + }, + { + "cell_type": "code", + "execution_count": 148, + "id": "633db4db", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "category\n", + "Miscellaneous 8\n", + "Food 2\n", + "Electronics 1\n", + "Kitchen 1\n", + "Name: count, dtype: int64" + ] + }, + "execution_count": 148, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[\"category\"].value_counts()" + ] + }, + { + "cell_type": "markdown", + "id": "17182206", + "metadata": {}, + "source": [ + "## Total Stock by Category using Matplotlib\n", + "\n", + "This shows the total available quantity for each category." + ] + }, + { + "cell_type": "code", + "execution_count": 149, + "id": "ee3ecf73", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0kAAAIXCAYAAABJihVzAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAVQZJREFUeJzt3QmcjeX///HP2Ma+L2MnkaVNlPWbEkkoS6FUQiVZQogKEZGyRKEkS+WrVJSESqVkLNlbCNnKnmWKLD9z/o/39fvd53+PJUMzzpk5r+fjcTJzzpkz15yZ07nf9/W5PldUIBAIGAAAAADASfO//wAAAAAACEkAAAAAcBpmkgAAAADAh5AEAAAAAD6EJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIARJCvv/7aoqKi3L8pcdzvv//+Jf1aAEBkIiQBQDLTAXpiLokJLs8//7zNmjXrkvzO1q1bZ3fddZcVL17cMmbMaIULF7a6devamDFjQjamlGD16tV23333WdGiRS06Otpy585tderUsUmTJtmpU6cu+PF4fgHg0ksXgu8JABHlrbfeSvD51KlT7fPPPz/j+nLlyiXqgFnBpXHjxpacFi9ebDfffLMVK1bMHn74YYuJibEdO3bYkiVL7OWXX7bOnTtf8jGlBG+88YY9+uijVqBAAbv//vutdOnS9ueff9qCBQusXbt2tmvXLnvqqacu6DF5fgHg0iMkAUAy06yCn4KGQtLp14eTwYMHW44cOWz58uWWM2fOBLft3bs3ZOMKZ/q9KiBVq1bNPv30U8uWLVvwtq5du9r3339vP/zwg6VWR44csSxZsoR6GACQJCi3A4AwOcB84okngiVaV1xxhb300ksWCASC91FJnu43ZcqUYInegw8+6G7btm2bPfbYY+7rMmXKZHny5LG7777btm7delHj2bx5s1WoUOGMgCT58+dP1Jhk1apVVr9+fcuePbtlzZrVbrnlFhcmTnfo0CHr1q2blShRwv38RYoUsQceeMD2799/zjEeP37cGjZs6MKcZr7OR6VumsXRrJgO5u+44w43O+bp37+/pU+f3vbt23fG1z7yyCPuuTh27Ng5H3/AgAHu53/nnXcSBCRP5cqVEzw3+v1Wr17d/a70O6tUqdIZ66bO9/z+/vvv1rZtWzdzpedNv7M333zzjO+tvw/9vPq59fvTcz1//vyzlnnOmDHDjUVjyps3rwvz+j5+GoN+n/o7uf32293P26pVq3/9HAJAuGAmCQBCTEFIB7BfffWVK8m69tpr3QFsz5493cHpyJEj3f1UnvfQQw/ZDTfc4A44pVSpUu5fzfgoKLRs2dIFDIWjcePG2U033WQ//fSTZc6c+YLGpHVIsbGxbubjyiuvPOf9/mlMP/74o/3nP/9xAalXr17u4Pm1115zY1q4cKFVqVLF3e+vv/5y9/v555/dAf91113nwtHHH39sv/32mztQP93ff/9td955p5ud+eKLL+z6669P1OyYQsGTTz7pZsNGjRrl1gppDZECgcrjBg4caO+++6516tQp+HUnTpxw4aVZs2ZubdbZHD161JXU3Xjjja5EMTFUtqjfu8KFvsf06dNdsP3kk0+sQYMG531+9+zZY1WrVnU/k8abL18+mzt3rvsbiouLc7NXopBVu3ZtV+r3+OOPu5A4bdo09/d2usmTJ1ubNm3c8zlkyBD3PTTO7777zgVef2j+n//5H6tXr57VrFnTBT79jWkW7WKfQwAIKwEAwCXVsWNHTQ8FP581a5b7fNCgQQnud9dddwWioqICmzZtCl6XJUuWQOvWrc94zKNHj55xXWxsrHvcqVOnBq/76quv3HX695989tlngbRp07pLtWrVAr169QrMnz8/cOLEiTPue64xNW7cOJAhQ4bA5s2bg9ft3LkzkC1btsCNN94YvK5fv35uTB9++OEZjxEfH59g3DNmzAj8+eefgVq1agXy5s0bWLVq1T/+HP6vLVy4cCAuLi54/Xvvveeuf/nll4PX6WetUqVKgq/XuM73nK1Zs8bd5/HHHw8k1um/Mz23V155ZaB27dqJen7btWsXKFiwYGD//v0Jrm/ZsmUgR44cwccfPny4G5v+zjx///13oGzZsgl+Ln3//PnzuzHods8nn3zi7qffk0fj0XW9e/c+Y1wX+xwCQDih3A4AQkzrV9KmTWtdunRJcL3K7zTLpNmB89FMiOfkyZP2xx9/2OWXX+7O/K9cufKCx6QudppJ0kzHmjVrbNiwYW7WQB3uNMOTmNK2zz77zDVzuOyyy4LXFyxY0O69915btGiRm+2QDz74wK655hpr0qTJGY+jWRK/w4cP26233mrr1693ZWKadUssle/5y+DUbELj0fPvv8/SpUtdGZlH5XMqg6xVq9Y5H9v7Wc5WZpeY39nBgwfdz6YZtcT8vvR3oeetUaNG7mPNvHkX/Z70WN7jzJs3z/3e9Lv0aDZHDTn8NCunGTaVbfpnezSrVbZsWZszZ84Z4+jQocMZ113scwgA4YSQBAAhpvUihQoVOuMA2+t2p9vPR+Vn/fr1C65pUomayq+01kcHzBdDJVcffvihO4BftmyZ9enTx3VqU7hQCd8/0ZoUlaBpjdTp9HPFx8cH1wPpYPqfSvr8VEKm0kKV2Gn9zYVQp7nTA5iCpH/dVosWLdzzp4N60XOn8jeVxJ0e2PxUUih6fhJLj6tyOQUStQnX70slkon5fen51e/29ddfd1/nv6hczt9gQ38/KtE7ffz62f28v7Oz/c4Ukk7/O0yXLp0r7TzdxT6HABBOCEkAkAqoJbfW3DRv3tzee+89N4ujDnpqCqBA8m9kyJDBBSa1otZBvGaqtLg/FLQOSTMnQ4cO/dc/19nkypXLNYPwDvC1jkYNIs7XiVCBQ6FBe0slxrfffutmdhSQxo4d62az9PvSLJu/Wce5eD+7xqWvO9ulRo0alpwUhNKkSZNkzyEAhBMaNwBAiKlJgmZGNAvhn01SSZl3u+dcZ+J1INq6dWsbPnx48Dp1EdNsQ1JShzZRE4B/GpNmNLSQf8OGDWfcpp9LB9ea9RLNciS2NbbK91Rup+5qeq4U2hJr48aNCT5XGNm0aZNdffXVZ5SLKYxpxkoH+hUrVjzvrJV+VjVH+PLLL90MmfeznYtK5RSQ1KBDYcOjDWdPd67nVz+/yhrVfOKf6O9HM3/6ef2PpZ/99PuJfmf6Wfx0nf/v8Hwu5jkEgHDCTBIAhJhaKOtg95VXXklwvbra6aBWLbQ9auF8tuCjNU2nz0CMGTPGPe7FUOezs81oeOt3/CVZZxuTxqMw89FHHyUoZ1O3NHVWU0c0r0RNHc+07mnmzJlnfL+zjUEH4KNHj7bx48e7TnWJpU18/eVwCpYKe/7nV/S5yhVfeOEF14UvsTMgan+t8apLnjr2nW7FihWulbf3/Oh36//96HmaNWvWGV93rudXz5vC1tkCpr8Ft9YoqUuify2ZAvSECRPOCMBqD67nVTM/Hq2JU+dBr+NeYlzscwgA4YKZJAAIMS2+v/nmm+3pp592B8pqYqByOQUMrcHxWj6L9q/RrNOIESPcOqaSJUu6Vtoqb1K7aO0ZVL58edd0QfdTud3Flu9pTZGaKWg9ilo4q8W4WjtrLyNv3cs/jWnQoEGu7EuBSM0AVI6mFuA6AFcjCI9anSuwqP21WoDr8Q4cOOAO6nXArufjdGovrWYJes70M2v/o/PRuh+NRWNXWFMLcJXJnd7AQK3K1UpdoVVh5J577knUc6Y9j1599VX3s+o5U1jSOigFMzWZ0M+j50QUOPR83Xbbba7ETuuH9LUaz9q1axM87rmeX5UcKszqY/0M+r3reVPDBt1fH0v79u3dz6KfQy3A1axCsztecwZvdkk/t0KNnh81WND9vRbg+p1rb6XEutjnEADCRqjb6wFApLcAF7W17tatW6BQoUKB9OnTB0qXLh148cUXgy2wPevXr3ftszNlyuQew2sNffDgwUCbNm1cW+ysWbMG6tWr5+5bvHjxBO2jE9sCfO7cuYG2bdu6NtF6PLXyvvzyywOdO3cO7NmzJ1FjkpUrV7qx6DEyZ84cuPnmmwOLFy8+4/v98ccfgU6dOrk23fpeRYoUcY/jtbf2twD3U2tyXf/KK6+c82fxvva///1voE+fPq7NtcbaoEGDwLZt2876NcuWLXNfc+uttwYu1IoVKwL33ntv8HeZK1euwC233BKYMmVK4NSpU8H7TZw40f2eo6Oj3fM8adKkQP/+/c/42/in51e/C/09FS1a1H2vmJgY971ef/31BI/x66+/up9Xj5EvX77AE088Efjggw/c4y1ZsiTBfd99991AxYoV3bhy584daNWqVeC3335LcB+NQa3J/8m/eQ4BINSi9J9QBzUAAMKJyv/UXlwlepoRSo00k6bZIW3YqxbhSS0SnkMAqRdrkgAAOI3W62TNmtWaNm2aKp4btYj305oklT6qHDA5AlJqfA4BRBbWJAEA8H9mz57tOsFp/yGte1LThNRAQaVYsWJuZkf7Fr399tuuy6DXpjsppdbnEEBkodwOAID/owYFalagjnBqhHH6Br8pubTujTfecI1B1FFPTR569erlNn5Naqn1OQQQWQhJAAAAAODDmiQAAAAA8CEkAQAAAEAkNW6Ij4+3nTt3uppob8M8AAAAAJEnEAi4Tb61OXeaNGkiNyQpIBUtWjTUwwAAAAAQJnbs2GFFihSJ3JDkddXRE5E9e/ZQDwcAAABAiMTFxbkJlPN13kz1IckrsVNAIiQBAAAAiDrPMhwaNwAAAACADyEJAAAAAMIlJGnX7759+1rJkiUtU6ZMVqpUKXvuuedc1wmPPu7Xr58VLFjQ3adOnTq2cePGUA4bAAAAQCoW0pD0wgsv2Lhx4+yVV16xn3/+2X0+bNgwGzNmTPA++nz06NE2fvx4W7p0qWXJksXq1atnx44dC+XQAQAAAKRSUQH/tM0l1rBhQytQoIBNnDgxeF2zZs3cjNHbb7/tZpHUw/yJJ56wHj16uNsPHz7svmby5MnWsmXLRHWwyJEjh/s6GjcAAAAAkSsukdkgpDNJ1atXtwULFtgvv/ziPl+zZo0tWrTI6tev7z7fsmWL7d6925XYefRDValSxWJjY8/6mMePH3c/vP8CAAAAAIkV0hbgvXv3diGmbNmyljZtWrdGafDgwdaqVSt3uwKSaObIT597t51uyJAhNmDAgEswegAAAACpUUhnkt577z175513bNq0abZy5UqbMmWKvfTSS+7fi9WnTx83feZdtIksAAAAAKSImaSePXu62SRvbdFVV11l27Ztc7NBrVu3tpiYGHf9nj17XHc7jz6/9tprz/qY0dHR7gIAAAAAKW4m6ejRo5YmTcIhqOwuPj7efazW4ApKWrfkUXmeutxVq1btko8XAAAAQOoX0pmkRo0auTVIxYoVswoVKtiqVatsxIgR1rZtW3d7VFSUde3a1QYNGmSlS5d2oUn7KqnjXePGjUM5dAAAAACpVEhDkvZDUuh57LHHbO/evS78tG/f3m0e6+nVq5cdOXLEHnnkETt06JDVrFnT5s2bZxkzZgzl0AEAAACkUiHdJ+lSYJ8kAAAAAClmnyQAAAAACDeEJAAAAAAIlzVJAAAAwIUo0XsOT1gIbR3aICKef2aSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAA8CEkAQAAAIAPIQkAAAAAfAhJAAAAAOBDSAIAAAAAH0ISAAAAAPgQkgAAAADAh5AEAAAAAD6EJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAIFxCUokSJSwqKuqMS8eOHd3tx44dcx/nyZPHsmbNas2aNbM9e/aEcsgAAAAAUrmQhqTly5fbrl27gpfPP//cXX/33Xe7f7t162azZ8+2GTNm2MKFC23nzp3WtGnTUA4ZAAAAQCqXLpTfPF++fAk+Hzp0qJUqVcpq1aplhw8ftokTJ9q0adOsdu3a7vZJkyZZuXLlbMmSJVa1atUQjRoAAABAahY2a5JOnDhhb7/9trVt29aV3K1YscJOnjxpderUCd6nbNmyVqxYMYuNjT3n4xw/ftzi4uISXAAAAAAgxYWkWbNm2aFDh+zBBx90n+/evdsyZMhgOXPmTHC/AgUKuNvOZciQIZYjR47gpWjRosk+dgAAAACpR9iEJJXW1a9f3woVKvSvHqdPnz6uVM+77NixI8nGCAAAACD1C+maJM+2bdvsiy++sA8//DB4XUxMjCvB0+ySfzZJ3e1027lER0e7CwAAAACk2JkkNWTInz+/NWjQIHhdpUqVLH369LZgwYLgdRs2bLDt27dbtWrVQjRSAAAAAKldyGeS4uPjXUhq3bq1pUv3/4ej9UTt2rWz7t27W+7cuS179uzWuXNnF5DobAcAAAAg1YYkldlpdkhd7U43cuRIS5MmjdtEVl3r6tWrZ2PHjg3JOAEAAABEhqhAIBCwVEwtwDUrpSYOmo0CAABAylWi95xQDyGibR36/5fHpOZsEBZrkgAAAAAgXBCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAA8CEkAQAAAIAPIQkAAAAAfAhJAAAAAOBDSAIAAAAAH0ISAAAAAPgQkgAAAADAh5AEAAAAAD6EJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAACKeQ9Pvvv9t9991nefLksUyZMtlVV11l33//ffD2QCBg/fr1s4IFC7rb69SpYxs3bgzpmAEAAACkXiENSQcPHrQaNWpY+vTpbe7cufbTTz/Z8OHDLVeuXMH7DBs2zEaPHm3jx4+3pUuXWpYsWaxevXp27NixUA4dAAAAQCqVLpTf/IUXXrCiRYvapEmTgteVLFkywSzSqFGj7JlnnrE777zTXTd16lQrUKCAzZo1y1q2bBmScQMAAABIvUI6k/Txxx9b5cqV7e6777b8+fNbxYoVbcKECcHbt2zZYrt373Yldp4cOXJYlSpVLDY29qyPefz4cYuLi0twAQAAAIAUEZJ+/fVXGzdunJUuXdrmz59vHTp0sC5dutiUKVPc7QpIopkjP33u3Xa6IUOGuCDlXTRTBQAAAAApIiTFx8fbddddZ88//7ybRXrkkUfs4YcfduuPLlafPn3s8OHDwcuOHTuSdMwAAAAAUreQhiR1rCtfvnyC68qVK2fbt293H8fExLh/9+zZk+A++ty77XTR0dGWPXv2BBcAAAAASBEhSZ3tNmzYkOC6X375xYoXLx5s4qAwtGDBguDtWmOkLnfVqlW75OMFAAAAkPqFtLtdt27drHr16q7crnnz5rZs2TJ7/fXX3UWioqKsa9euNmjQILduSaGpb9++VqhQIWvcuHEohw4AAAAglQppSLr++utt5syZbh3RwIEDXQhSy+9WrVoF79OrVy87cuSIW6906NAhq1mzps2bN88yZswYyqEDAAAASKWiAtqMKBVTeZ663KmJA+uTAAAAUrYSveeEeggRbevQBhYJ2SCka5IAAAAAINwQkgAAAADAh5AEAAAAAD6EJAAAAAAgJAEAAADA2TGTBAAAAAA+hCQAAAAA8CEkAQAAAIAPIQkAAAAAfAhJAAAAAOBDSAIAAAAAH0ISAAAAAPgQkgAAAADAh5AEAAAAAD6EJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAA8CEkAQAAAIAPIQkAAAAAfAhJAAAAAOBDSAIAAACAfxOS+vfvb9u2bbvQLwMAAACA1BmSPvroIytVqpTdcsstNm3aNDt+/PhFf/Nnn33WoqKiElzKli0bvP3YsWPWsWNHy5Mnj2XNmtWaNWtme/bsuejvBwAAAABJHpJWr15ty5cvtwoVKtjjjz9uMTEx1qFDB3fdxdDj7Nq1K3hZtGhR8LZu3brZ7NmzbcaMGbZw4ULbuXOnNW3a9KK+DwAAAAAk25qkihUr2ujRo11omThxov32229Wo0YNu/rqq+3ll1+2w4cPJ/qx0qVL54KWd8mbN6+7Xo+hxx4xYoTVrl3bKlWqZJMmTbLFixfbkiVLLmbYAAAAAJC8jRsCgYCdPHnSTpw44T7OlSuXvfLKK1a0aFF79913E/UYGzdutEKFCtlll11mrVq1su3bt7vrV6xY4R67Tp06wfuqFK9YsWIWGxt7zsdT+V9cXFyCCwAAAAAka0hSgOnUqZMVLFjQlcRpZunnn392JXEKPYMHD7YuXbqc93GqVKlikydPtnnz5tm4ceNsy5Yt9p///Mf+/PNP2717t2XIkMFy5syZ4GsKFCjgbjuXIUOGWI4cOYIXBTYAAAAASKx0doGuuuoqW79+vd16662uHK5Ro0aWNm3aBPe555573Hql86lfv37wY5XqKTQVL17c3nvvPcuUKZNdjD59+lj37t2Dn2smiaAEAAAAINlCUvPmza1t27ZWuHDhc95H64ri4+Mv9KHdrFGZMmVs06ZNVrduXVfGd+jQoQSzSepup7VL5xIdHe0uAAAAAHBJyu28tUen+/vvv23gwIH/6rfw119/2ebNm10Znxo1pE+f3hYsWBC8fcOGDW7NUrVq1f7V9wEAAACAJAtJAwYMcGHmdEePHnW3XYgePXq4dUxbt251XeuaNGniSvdUrqf1RO3atXOlc1999ZVbB9WmTRsXkKpWrXqhwwYAAACA5Cm300ySNn093Zo1ayx37twX9FhqHa5A9Mcff1i+fPmsZs2arr23PpaRI0damjRp3Cay6lpXr149Gzt27IUOGQAAAACSPiSpxE7hSBetG/IHpVOnTrnZpUcffTTx39nMpk+f/o+3Z8yY0V599VV3AQAAAICwCkmjRo1ys0hq2qCyOpXDedSqu0SJEqwVAgAAABA5Ial169bu35IlS1r16tVdUwUAAAAAiMiQpL2GsmfP7j7WxrHqZKfL2Xj3AwAAAIBUG5K0HmnXrl2WP39+t2fR2Ro3eA0dtD4JAAAAAFJ1SPryyy+DnevUjhsAAAAAIjok1apVK/ix1iQVLVr0jNkkzSTt2LEj6UcIAAAAAOG8maxC0r59+864/sCBA+42AAAAAIiokHSuzWS1T5L2NQIAAACAiGgB3r17d/evAlLfvn0tc+bMwdvUrGHp0qV27bXXJs8oAQAAACDcQtKqVauCM0nr1q1zG8h69PE111xjPXr0SJ5RAgAAAEC4hSSvq12bNm3s5ZdfZj8kAAAAAJEdkjyTJk1KnpEAAAAAQEoMSUeOHLGhQ4faggULbO/evRYfH5/g9l9//TUpxwcAAAAA4R2SHnroIVu4cKHdf//9VrBgwbN2ugMAAACAiAlJc+fOtTlz5liNGjWSZ0QAAAAAkJL2ScqVK5flzp07eUYDAAAAACktJD333HPWr18/O3r0aPKMCAAAAABSUrnd8OHDbfPmzVagQAErUaKEpU+fPsHtK1euTMrxAQAAAEB4h6TGjRsnz0gAAAAAICWGpP79+yfPSAAAAAAgJa5JAgAAAIDU7IJnkk6dOmUjR4609957z7Zv324nTpxIcPuBAweScnwAAAAAEN4zSQMGDLARI0ZYixYt7PDhw9a9e3dr2rSppUmTxp599tnkGSUAAAAAhGtIeuedd2zChAn2xBNPWLp06eyee+6xN954w7UFX7JkSfKMEgAAAADCNSTt3r3brrrqKvdx1qxZ3WySNGzY0ObMmZP0IwQAAACAcA5JRYoUsV27drmPS5UqZZ999pn7ePny5RYdHZ30IwQAAACAcA5JTZo0sQULFriPO3fubH379rXSpUvbAw88YG3btk2OMQIAAABA+Ha3Gzp0aPBjNW8oVqyYxcbGuqDUqFGjpB4fAAAAAIR3SDpdtWrV3AUAAAAAIjIkTZ069R9vV9kdAAAAAERMSHr88ccTfH7y5Ek7evSoZciQwTJnzkxIAgAAABBZjRsOHjyY4PLXX3/Zhg0brGbNmvbf//43eUYJAAAAAOEaks5GTRvU0OH0WSYAAAAAiMiQJOnSpbOdO3cm1cMBAAAAQMpYk/Txxx8n+DwQCLjNZV955RWrUaNGUo4NAAAAAMI/JDVu3DjB51FRUZYvXz6rXbu2DR8+PCnHBgAAAADhX24XHx+f4HLq1CnbvXu3TZs2zQoWLHjRA9GaJgWurl27Bq87duyYdezY0fLkyWNZs2a1Zs2a2Z49ey76ewAAAABAsq1J2r9/v8XFxVlSWL58ub322mt29dVXJ7i+W7duNnv2bJsxY4YtXLjQrXlq2rRpknxPAAAAAPjXIenQoUNuZidv3rxWoEABy5Url8XExFifPn3cXkkXQy3EW7VqZRMmTHCP5zl8+LBNnDjRRowY4Ur5KlWqZJMmTbLFixfbkiVLLup7AQAAAECSrUk6cOCAVatWzX7//XcXasqVK+eu/+mnn2zMmDH2+eef26JFi2zt2rUuxHTp0iVRj6vQ1aBBA6tTp44NGjQoeP2KFSvcRrW63lO2bFkrVqyYxcbGWtWqVc/6eMePH3cXT1LNdgEAAACIDIkOSQMHDrQMGTLY5s2b3SzS6bfdeuutdv/999tnn31mo0ePTtRjTp8+3VauXOnK7U6ndU76fjlz5kxwvb63bjuXIUOG2IABAxL7YwEAAADAxZXbzZo1y1566aUzApKo5G7YsGH2wQcfWPfu3a1169bnfbwdO3a4zWffeecdy5gxoyUVlf6pVM+76PsAAAAAQJKHJO2FVKFChXPefuWVV1qaNGmsf//+iXo8ldPt3bvXrrvuOrcRrS5qzqBZKH2sMHbixAm3DspP3e0Uys4lOjrasmfPnuACAAAAAEkektSsYevWree8fcuWLZY/f/5Ef+NbbrnF1q1bZ6tXrw5eKleu7NY7eR+nT5/eFixYEPyaDRs22Pbt293aKAAAAAAI6ZqkevXq2dNPP+0aNGitkJ8aJfTt29duu+22RH/jbNmyudknvyxZsrg9kbzr27Vr58r3cufO7WaEOnfu7ALSuZo2AAAAAMAlbdyg2Z3SpUu7jnTqNBcIBOznn3+2sWPHuqA0depUS0ojR450JXzaRFaPr6Cm7wUAAAAAySUqoKSTSCqpe+yxx1wHO+/LoqKirG7duvbKK6/Y5ZdfbuFGLcBz5MjhmjiwPgkAACBlK9F7TqiHENG2Dm1gKVlis0GiZ5KkZMmSNnfuXDt48KBt3LjRXadgpHI4AAAAAEgNLigkeXLlymU33HBD0o8GAAAAAFJKdzsAAAAAiASEJAAAAADwISQBAAAAgA8hCQAAAAAutHHDxx9/bIl1xx13JPq+AAAAAJAiQ1Ljxo0T9WDaM+nUqVP/dkwAAAAAEN4hKT4+PvlHAgAAAABhgDVJAAAAAPBvN5M9cuSILVy40LZv324nTpxIcFuXLl0u5iEBAAAAIGWGpFWrVtntt99uR48edWEpd+7ctn//fsucObPlz5+fkAQAAAAgssrtunXrZo0aNbKDBw9apkyZbMmSJbZt2zarVKmSvfTSS8kzSgAAAAAI15C0evVqe+KJJyxNmjSWNm1aO378uBUtWtSGDRtmTz31VPKMEgAAAADCNSSlT5/eBSRReZ3WJUmOHDlsx44dST9CAAAAAAjnNUkVK1a05cuXW+nSpa1WrVrWr18/tybprbfesiuvvDJ5RgkAAAAA4TqT9Pzzz1vBggXdx4MHD7ZcuXJZhw4dbN++ffbaa68lxxgBAAAAIHxnkipXrhz8WOV28+bNS+oxAQAAAEDKmUmqXbu2HTp06Izr4+Li3G0AAAAAEFEh6euvvz5jA1k5duyYffvtt0k1LgAAAAAI73K7tWvXBj/+6aefbPfu3cHPT5065cruChcunPQjBAAAAIBwDEnXXnutRUVFucvZyuq0seyYMWOSenwAAAAAEJ4hacuWLRYIBOyyyy6zZcuWWb58+YK3ZciQwTVx0OayAAAAABARIal48eLu3/j4+OQcDwAAAACkrBbgsnnzZhs1apT9/PPP7vPy5cvb448/bqVKlUrq8QEAAABAeHe3mz9/vgtFKrm7+uqr3WXp0qVWoUIF+/zzz5NnlAAAAAAQrjNJvXv3tm7dutnQoUPPuP7JJ5+0unXrJuX4AAAAACC8Z5JUYteuXbszrm/btq1rDQ4AAAAAERWS1NVu9erVZ1yv69ThDgAAAAAiotxu4MCB1qNHD3v44YftkUcesV9//dWqV6/ubvvuu+/shRdesO7duyfnWAEAAAAg2UUFtPlRImgPpF27drmZJHW2Gz58uO3cudPdVqhQIevZs6d16dLFbTYbTuLi4ixHjhx2+PBhy549e6iHAwAAgH+hRO85PH8htHVogxT9/Cc2GyR6JsnLUgpBatygy59//umuy5YtW1KMGQAAAABSVne702eJCEcAAAAAIjoklSlT5rzldAcOHPi3YwIAAACAlBGSBgwY4Gr4AAAAACC1uqCQ1LJlS9p8AwAAAEjVEr1PUnJ0rRs3bpxdffXVrrOELtWqVbO5c+cGbz927Jh17NjR8uTJY1mzZrVmzZrZnj17knwcAAAAAHDBISmRncIvSJEiRWzo0KG2YsUK+/7776127dp255132o8//uhuVwe92bNn24wZM2zhwoWu5XjTpk2TfBwAAAAAcMH7JF0quXPnthdffNHuuusutyfTtGnT3Meyfv16K1eunMXGxlrVqlUT9XjskwQAAJB6sE9SaG2NkH2SEj2TlNxOnTpl06dPtyNHjriyO80unTx50urUqRO8T9myZa1YsWIuJJ3L8ePH3Q/vvwAAAABAYoU8JK1bt86tN4qOjrZHH33UZs6caeXLl7fdu3dbhgwZLGfOnAnuX6BAAXfbuQwZMsSlQ+9StGjRS/BTAAAAAEgtQh6SrrjiClu9erUtXbrUOnToYK1bt7affvrpoh+vT58+bvrMu+zYsSNJxwsAAAAgdbugFuDJQbNFl19+ufu4UqVKtnz5cnv55ZetRYsWduLECTt06FCC2SR1t4uJiTnn42lGShcAAAAASJEzSaeLj49364oUmNKnT28LFiwI3rZhwwbbvn27W7MEAAAAAKluJkmlcfXr13fNGP7880/Xye7rr7+2+fPnu/VE7dq1s+7du7uOd+o+0blzZxeQEtvZDgAAAABSVEjau3evPfDAA7Zr1y4XirSxrAJS3bp13e0jR460NGnSuE1kNbtUr149Gzt2bCiHDAAAACCVC7t9kpIa+yQBAACkHuyTFFpb2ScJAAAAACJP2DVuAAAAAIBQIiQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+6fyfAACA8FWi95xQDyHibR3aIOKfAyASMJMEAAAAAD6EJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAACJeQNGTIELv++ustW7Zslj9/fmvcuLFt2LAhwX2OHTtmHTt2tDx58ljWrFmtWbNmtmfPnpCNGQAAAEDqFtKQtHDhQheAlixZYp9//rmdPHnSbr31Vjty5EjwPt26dbPZs2fbjBkz3P137txpTZs2DeWwAQAAAKRi6UL5zefNm5fg88mTJ7sZpRUrVtiNN95ohw8ftokTJ9q0adOsdu3a7j6TJk2ycuXKuWBVtWrVEI0cAAAAQGoVVmuSFIokd+7c7l+FJc0u1alTJ3ifsmXLWrFixSw2Nvasj3H8+HGLi4tLcAEAAACAFBeS4uPjrWvXrlajRg278sor3XW7d++2DBkyWM6cORPct0CBAu62c61zypEjR/BStGjRSzJ+AAAAAKlD2IQkrU364YcfbPr06f/qcfr06eNmpLzLjh07kmyMAAAAAFK/kK5J8nTq1Mk++eQT++abb6xIkSLB62NiYuzEiRN26NChBLNJ6m6n284mOjraXQAAAAAgxc0kBQIBF5BmzpxpX375pZUsWTLB7ZUqVbL06dPbggULgtepRfj27dutWrVqIRgxAAAAgNQuXahL7NS57qOPPnJ7JXnrjLSWKFOmTO7fdu3aWffu3V0zh+zZs1vnzp1dQKKzHQAAAIBUF5LGjRvn/r3pppsSXK823w8++KD7eOTIkZYmTRq3iaw619WrV8/Gjh0bkvECAAAASP3Shbrc7nwyZsxor776qrsAAAAAQMR0twMAAACAcEBIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAA8CEkAQAAAIAPIQkAAAAAfAhJAAAAAOBDSAIAAAAAH0ISAAAAAPgQkgAAAADAh5AEAAAAAD6EJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAIFxC0jfffGONGjWyQoUKWVRUlM2aNSvB7YFAwPr162cFCxa0TJkyWZ06dWzjxo0hGy8AAACA1C+kIenIkSN2zTXX2KuvvnrW24cNG2ajR4+28ePH29KlSy1LlixWr149O3bs2CUfKwAAAIDIkC6U37x+/frucjaaRRo1apQ988wzduedd7rrpk6dagUKFHAzTi1btrzEowUAAAAQCcJ2TdKWLVts9+7drsTOkyNHDqtSpYrFxsae8+uOHz9ucXFxCS4AAAAAkOJDkgKSaObIT597t53NkCFDXJjyLkWLFk32sQIAAABIPcI2JF2sPn362OHDh4OXHTt2hHpIAAAAAFKQsA1JMTEx7t89e/YkuF6fe7edTXR0tGXPnj3BBQAAAABSfEgqWbKkC0MLFiwIXqf1RepyV61atZCODQAAAEDqFdLudn/99Zdt2rQpQbOG1atXW+7cua1YsWLWtWtXGzRokJUuXdqFpr59+7o9lRo3bhzKYQMAAABIxUIakr7//nu7+eabg593797d/du6dWubPHmy9erVy+2l9Mgjj9ihQ4esZs2aNm/ePMuYMWMIRw0AAAAgNQtpSLrpppvcfkjnEhUVZQMHDnQXAAAAAIjoNUkAAAAAEAqEJAAAAADwISQBAAAAgA8hCQAAAAB8CEkAAAAA4ENIAgAAAAAfQhIAAAAA+BCSAAAAAMCHkAQAAAAAPoQkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+6fyfAEC4KtF7TqiHEPG2Dm0Q8c8BACAyMJMEAAAAAD6EJAAAAADwISQBAAAAgA9rklIA1mKEHmsxAAAAIgczSQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAAAAA+hCQAAAAA8CEkAQAAAIAPIQkAAAAAfAhJAAAAAOBDSAIAAAAAH0ISAAAAAPgQkgAAAADAh5AEAAAAAD6EJAAAAABIaSHp1VdftRIlSljGjBmtSpUqtmzZslAPCQAAAEAqFfYh6d1337Xu3btb//79beXKlXbNNddYvXr1bO/evaEeGgAAAIBUKOxD0ogRI+zhhx+2Nm3aWPny5W38+PGWOXNme/PNN0M9NAAAAACpUDoLYydOnLAVK1ZYnz59gtelSZPG6tSpY7GxsWf9muPHj7uL5/Dhw+7fuLg4S6nijx8N9RAiXkr++0kteB2EHq+D0ON1EHq8DkKP10FoxaXwYyJv/IFAIOWGpP3799upU6esQIECCa7X5+vXrz/r1wwZMsQGDBhwxvVFixZNtnEi9csxKtQjAEKP1wHA6wDIkUqOif7880/LkSNHygxJF0OzTlrD5ImPj7cDBw5Ynjx5LCoqKqRji1RK7AqpO3bssOzZs4d6OEBI8DpApOM1APA6CAeaQVJAKlSo0D/eL6xDUt68eS1t2rS2Z8+eBNfr85iYmLN+TXR0tLv45cyZM1nHicRRQCIkIdLxOkCk4zUA8DoItX+aQUoRjRsyZMhglSpVsgULFiSYGdLn1apVC+nYAAAAAKROYT2TJCqda926tVWuXNluuOEGGzVqlB05csR1uwMAAACAiAtJLVq0sH379lm/fv1s9+7ddu2119q8efPOaOaA8KXyR+1zdXoZJBBJeB0g0vEaAHgdpCRRgfP1vwMAAACACBLWa5IAAAAA4FIjJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAhRx8pAOGEkAQAAEJm9OjRtnHjRouKiiIoAQgbhCQAABAScXFx9vbbb1uNGjVsy5YtBCUAYYOQhBRRekEZBnD218ePP/5oy5Yt4+lBipQ9e3Z77733rGLFii4o/frrrwQlIIlxDHVxCEkI2xfz33//HbxOZRjx8fEhHBUQXq8RvSY+/PBDa9CggX3zzTe2ffv2UA8LuCDe/9NLlChhY8aMsXLlyln9+vVt27ZtBCUgid8v9D6hExJIPEISwo5ezJ9++qk1adLE7r77bnvhhRfc9WnSpCEoAf/3Gpk3b5498MAD1rNnT2vfvr0VK1YswXPDSQWkhL9jmT17tj3xxBPuY61Nuummm5hRApL4hFqzZs3syy+/dCchkDhRAebgEGYWL17s3iQ7dOjg3ih///13u/zyy4NnQHTwp8AERCL9L1uzrC1btrSyZcvasGHD7K+//nKvEx1s6rXRvXv34H29A1EgHOnsdp06ddxMUpUqVdz/81966SV3ILdo0SIrWbIkf8fAv/D1119bw4YN7ZVXXrHWrVuf9T2B94qzIyQhrKxfv96WLl1qBw4csG7dutmRI0ds5syZbjapTJky9sEHH7j7EZQQ6Vq0aGGZM2d2r5Px48fbL7/84ha+/8///I9Vq1bNpk+fHuohAuc1atQoF+4XLFgQvE5/y5ol3bt3rzvA0ywpB3HAhdPrZuDAgbZ161abNGmSHT582FatWmVTpkyxDBkyWNOmTa1evXo8tefA6XiEDb2I77nnHld2oYM/yZIli5si7t27t3vj1IGhMJOESOJN+K9Zs8a++uor9/G1117rXhPXXXed7d+/3x5++GF3+0MPPeRmmigSQErw559/2g8//BD8XH+3OiHWqVMn956ghg76lxlRIHH8/+/X60YnnefMmWMrVqywNm3a2JAhQ2zXrl22fPlyGzRokHsN4uwISQgb2bJls7vuussFpM8//zx4faZMmVxQeuqpp+y7775zZxiBSOGdQdcsqha1f/vtt660rk+fPm4GSTXmKkVt3ry5Zc2a1X777Td3hvDkyZOhHjpwXo0bN7a8efO6A7fjx48Hw5DK7G6//XarXbu2nThxgmcSSCS9hmJjY23cuHHu8/79+7slC5oxypgxoyvH1ppW3f7HH3+42SWcXbpzXA8kO3/5xKlTpyxPnjzWuXNnF5JUO9ulSxe3yaDoha0303Tp0lmlSpX47SBiXh9eVyKdAXzxxRftvvvuczOsctVVVwXvr+52et0oMClIKSgB4fb3rMYMBw8etLRp09rVV19t5cuXdwdvataj9wGFf4UifR4dHW1Tp051J8oAJM7Ro0ddaZ1KVdOnT++qC7TWW8sZtI7VoxNvOu7SCWqcHWuSENI3TNWha9ZI5RZaiK6GDfnz53eLeCdPnmw333xzMCgBkWDTpk3urJ//daIzf3v27LF33nkneD8dUOpAUxYuXOgC0oYNG9xBpUrxgHCcDdWJsBw5cri/VbWv18mwWrVq2dNPP21z5851YV8HcjqgU9i/5pprQj18IMX56aef7NVXX3Xh6JFHHnGNsDwKTx9//LELUirf5v3i3JhJQkjoDVMNGR588EFXJnTFFVfYk08+addff729+eab1rZtW3c/7cSuj3UdEAmL2FUn/vrrr7vZIm+mVeV13jo8r2mJF5B0MKmDTIUmreUoUqRISH8G4HT6O1ZDHv2/XN0YVUa3Y8cOe/75591Ff89Dhw51B3JaO6Ez2zVr1gyeLABwfvv27bN8+fK5jzVDqxMSel944403gjNKKsfWLK3K8VSh4K9GwJmYSUJIqL2rdxZRZzl0plHrKfS53jT1phoXF2cvv/yyO7uoQFWgQAF+W0jVNCNUsGBBF3ZUkpQrVy53vRax63WgRg0KR96Z+UOHDrm1HDrRQBkqwv0EgEpBta7UC//r1q2zHj16uP/3e51LAVy41atXu+MnXbS22/Pzzz+75gw6+fbcc8+55lfqGqkTE1oLiH9G4waEhM6Gq95cPft14Fe0aFFr1aqVO+DTG6he0NmzZ3dnQj755BMCEiKCZoQUkHSWT2uQ5s+f765XKZJeM3Xr1nXrNbyDTJ2Vf//99y0mJibEIwf+udOWDsq0VkJ7eon+nnUWu1evXu4kmA7mAFwcvbZUfaBmPh999FHw+nLlytljjz3myrUff/xxV2KnJQ0EpMQhJCEkb5pqSamuKmvXrnWlF7roxS0rV650Zx11W86cOS137tz8lhBR1MJbex5NnDjR1Y9rdkklE97Gylroftttt9lrr73mQlLhwoVDPWTgrLxAr4M1/T9dayHEKx/VAZvWIHnlowAuXPXq1a1v377u5PKIESNs1qxZwdsUiP7zn/+45Q1a543EIyQhWWljS+9Mos4cem+aKg3S/i7aYV0XrcHw3jR10Ld582b35glEIrU9Hj58uKsfV8mpSpRuueUW+/77790Mk+rNq1atakuWLHH7yADhQP+v9/5/rwoB/d1qLZJmPzULqs517dq1c81FVPKjlt/Tpk1z7xNeaSmA87/ORJvCahZWJ9N00llB6ZlnnnEd6/T+odeZZm71r044a913iRIleHovAGuSkCzUuUjNGE7vpqJZIZ0FV4MGldT17NnTzSrpjLg2xNSaDAWmRYsWufawQGrnrS/SG546e6nzV40aNdxC288++8yeffZZV26q0lOFJyAldLHTpuBeB0Zt4aD//6uUVGe71aRBB2tai6TZUZWVEvaBxNPJ5EcffdS9N2j9ql5rI0eOtHvvvdetT1JnO3VD1VIGBagvvviCLnYXIwAksWnTpgVuuOGGwIwZM9znX3zxRSBt2rSBxo0bB/LkyROoVatWYOLEie62b7/9NtCwYcNAtmzZAhUqVAjcfPPNgdWrV/M7QUTRayV37tyBwoULB8qUKRNo0aJF4O+//3a3zZ8/P1C9evXA3XffHZg7d27wa+Lj40M4YiAQOHXqlHsajhw5Enw6Fi9eHMiaNWtgwoQJgZ9//tl9Xrdu3UChQoUCmzZtcvf55ptvAm+99VZg8uTJgS1btvBUAhdAx0h58+YNTJkyJbBv377AiRMnAu3atQsULFgwMH36dHef3bt3B2JjY93n27Zt4/m9SMwkIcmpY1HXrl3dxq8qDVJJkGrOddZD5UO9e/d26y3UDlalF/Ljjz+61sU6C6maWiBSzrrrLKAamNx9991unzCdVdfMqsqPdPZdZ+G1l5heUypT1W1srolQ81rRr1ixwnXM0p53xYsXd3+fM2bMcH/H3jqjP//8020GrjPaqiDQLCmAi6M2+Wp48uWXX7qW395SBR1vqfpAlTyapcW/x5okJCmvY5E2ttRB4PTp011NurdZmYKQOthddtllbu+jcePGuesrVKjgyowISIi0vWO0mFYHjbfeeqsrjVBg0okEHVDecccdduzYMbeeQ6+pgQMHEpAQNgFpzZo1biF4o0aNXECS3bt3uxNlXkDSeiPte6T1ENrWYePGjSEePZBy1yGJlibs3LnTnUjzukaKGjacPHnSldYhaRCSkGydjLRwUAd4OtO4bNmy4H10IKiadNWnjx071i06BCLxQFMhSa2PdXbd2wdMgUnh6KmnnnIHlTfeeKN7HelglEW3CJeApE51WiiutXJaC+HRmlP9nao9vQ7YVFEgWkyur1VoAnBh4cg7thLtg6SApBNskjlzZnc/NWlQgwadcEbSICQhWRagr1+/PjijpIM7LeT98MMPg/dVy+IBAwa4Berq2gVEGh1oPvDAA9a9e3fX/eu+++4L3uYFJW0MqL0v1AkMCJe/2x07drj/bzds2NAGDx4cvG3MmDGuQkBNd+bNm+dOlIkO3tSFSwdz7OkFXNgxlZYsvPTSS+7kskpZ9Z6g151OVLRs2dKVbG/dutW99nRCrXTp0jzFSYQ1SUjSF7OCkM4samNY1cyqP7/OlGsTM9G6pKZNmwa/zut+BETKa0Sb+unsus6oa/ZIeyJNnjzZreVQW3y90Xl0Jl5veipXAsKFDsiaN2/u9u/S/+d1sktl1DpwU2dSzSSpFbHWKaksSC3rN23a5NZL0MUOSDwdUz388MNunyOtT/3kk0+sW7du7vWl23SyWa8xvRb1XqGTEXofQdIgJCHJaBGhatN1NrFBgwbB8iFRUNLCcwUinTFXm0og0gKSNvjr37+/ezPTYnadPGjfvr07w67NYtX+/oYbbnD/AuFMa4s005khQwb3//qPPvrI3nrrLbe2zps90vqkuXPnutmjypUrW8mSJUM9bCDF0F5jmrHVGtWOHTu6Ch0171EFgtZz631FJ5rVyEHlrHp9sbF40iIkIcloXwwtNtdZca9u3T9TpBe4amgLFSpkU6ZM4ew4IorOquvkgbdHzK+//uoC0/333++u0+tErwut5fBONgDhfhDXqVMnN3v03HPPufcAoUIA+Pe0b6QCUmxsrG3bts1q1qzp3hu0llu03luhCcnnf1dUAhd4Rly8ICTaOV2Lz70OR7pe9/UC0q5du1wbcAUonTWnfAiRwjtgfO+996xJkyZuRtVTqlQpu+eee1wTE12vMlWdmWfTWKQE+rvVGe3HHnvMnQSoUqWKO5DT37v/vQLAhdPaVB1Lfffdd6765vbbbw+ePPv+++/da08NfvQ+guRB4wYkmvemp9kilVHoxauGDDqbER0d7bpwaf8jnV0U3VdfozMgqlVXTbqCUrFixXjWETFdibQGyfvX2x9G65EUnu688073Jqc3Pi2+VVci7R2mFvlASqADNG/Lh0GDBrkDOiEgARfX4tuj94MjR47Ybbfd5srutG7VO/E8bdo0+/33312XOyQfQhISTW966rKlbnVvv/22W0OhDTC13ki0lkIzRlp4rs3MRAeCkyZNchsLatEhEEmvF+0Tpr3BtP6oWrVqNnv2bLeWQ40bvDdFlZ9qfzDv9cHBJVIaddMaPXq0OwnQo0cP140LwIWdgF68eLHbEmXChAnB/SO17k9BSev6vC0jVNaqqhx1vMudOzdPczKi3A6J8uOPP7oXbP78+V15kF6cCkx6Y/RaF6tlsc5saApYm5l5jRt0ZvGrr75yB4tApLzhacM/1ZRrDxmVl7Zo0cK++eYbV1L3zjvvBNu06rWl29k/BimZ/p5ffPFF69u3rwv+ABLH6wys9amamdVxlNYdqRtk27Zt7fDhwy48acbWqzJQo6wrr7ySpziZ0bgB5/Xqq6+6F6Q6bqmDimaJ1GJSJXZqQ6nQpPaTHtWm68Dv66+/dnsl6XaV2QGRQuvzvLVHmnHV5srea0OhSWHppptucmv5dNZdYeraa68N8aiBf097fmldHYDEnVDT+8BDDz1kdevWtcaNG7slCm3atHEdInXspZMOaru/b98+d0ItX7587lgMyY+QhHPyGjNoJkgzSDpTqPVIKg3S+iK1M9bZjg4dOrgXtD8oic6Me7utA6n5NXL6InXtZTFw4EBXGrF69eoEC2u1p4XOGq5bt8690amdKycRACDy6ARZz549Xevu559/PnhCbfv27dasWTMXlHRyjdnZ0GBNEs79x5Emjf30009uMa4O5pYtW+Y2EPz000/dC7lPnz5uKljldVOnTnXrkUQv9FWrVhGQEBGvEb2ZaeZU1MFOex81bNjQtW5VkxKV16nRiShM6c1ObZO1CFevFQISAESmTJkyuXK6efPmBZsy6OSb3jvUGEuNGdTm2zu+wqVFSMI/UjCKi4tziwM1K6QFhCof0plw0W7P6salAz6FJn2sEjwgtfM28lN5hNqzDh8+3JWWemVzTZs2da8PlR61bt3adbfTbNPJkydDPXQAQBjQ0gU1wipatKg7flIFjledoKCkLnbly5e3o0ePhnqoEYlyO/yjIUOGuECkdRM6y7F27Vrr1q2bO/B7+OGH3YGgaJ2F7nPgwAEbMWKEW4sERArNDqmRybPPPnvGSYIZM2a4Ft9Zs2Z1i29PL0sFAETWNio6htLnWr6gk20rV650lTraa1Lldf69xli6EDrMJCFIU7xy7Nix4HV6cebMmdO9YPVCvvrqq10I0uJctan0ZpQUnNT6++OPPyYgIWLo9aHXgi7a02Lu3Lm2Zs2aBHteqE2+2riqLE9ldnodAQAihxd45syZ4zoB16pVy6pXrx48AX399de7cm01bahXr557b/HWubK2O3QISfj/fwxp0rjWk1pI/vnnn7vrdEDn76KiIHXNNde4Vq86MNSZce0FI1myZHH1tUBq54UgdXpUGYTafWsmSU0Z2rdv72Zc/UHprrvuslGjRrmSPK/uHAAQGSefFXi0T17Lli3dmlW9F6ji5vbbbw+ebFZQUuWBuqOqjBuhR0hCAmpF+dtvv7nyOU3/+tu56uBOQcqro1WtrM6O68yIOrAAkXRGUG9sd955p+tip5MLen3oNaOwpI6PmlGS5557zrp372516tSxEiVKhHr4AIBkps6m4h0z6VhJ+0vq/ULrty+//HK35jtv3rxuTavCkVSuXNntK6mTagg91iThDGrvrbIgzQxp6lcHhdq0TC92XRSkdJCo8iJ1v1MjB2+DMyASaKZVJRPaTFlBSS3yNeuqoHTo0CG74YYb3MdajBsbG+ve9PTmBwBI3d59910bNmyY9ejRwzXzkc2bN7uqm86dO7sGWDfffLPVrFnTbRCrkmzNHunktHd/hAdmknAGneF4+eWX7e+//3blRApKmTNndqVEOmOuNUvqeKcXvTaaJSAhUuiEgbrT6ayfGpfoojOB/tIKreHTG57CU8WKFW3p0qUEJACIECqj0zKFSZMmBZcjaK+8+++/3zVqUKmd9p3UbFHGjBldswaVbWttt46tED7Y6RNndcUVV7iz5F27dnUld4899hgNGRDxNIOaPn16V0rh7W/ktWv11hrt2LHDzSCpM6Su98otAACpl9YcqbmVWnZrhkjHT2pwpfcBzRCppbcaMvzyyy9WsmRJ1/FU9N7x/vvvu/2QFKIQPnj3xj/OKKmTnQ7ytCP0t99+m+B2/8J0IFLojJ9CkNrd66yfZo8UnvR6UEB64YUXXMmqriMgAUDqt2LFCrfW6Omnn3brusuUKePK56Kjo92SBG9GSZ3qVH3z1ltvua0h2rZt6/ZJ0nX+JlkID4Qk/CO90PVC1tnzXr16udIhj9eeEkitvBMB6l6nUKQGJSo91aZ/s2bNcmWpXnmEXg+vv/66LVq0yK3nAwBEBs0CPfLII+5EmYKS/lVFjheUNKOkZlcydOhQV449fvx4t6RB+yLppDTCD40bkCjr16+3vn37ulpaTRkDkdLFTnt/DRo0yDUsUVOGJ5980h588EHX3U7/qoWrOkDqDKH2Sfr666/dWiQAQOqnagKvakBldpo10hokvW+o6kBBSOuN9B6iINWiRQt33z179riSO06qhS9CEhLN3w4ciATz58+3pk2bujc7dSDS/mBqVjJv3jzX0luB6JNPPnHldXpTfOihh6xcuXKhHjYA4BLSWiNv01et51aHO80OnR6UdD+tT2rTpg2/nxSAkAQAZq4tq3dGz9sAsHXr1lagQAG3v4XqzG+55Ra76aab7LXXXjtjxslrAQ4AiAze//9PD0rjxo1z647Uxc4LSmrYoHCUL18+mzp1Kk0aUgDWJAGIeOpE1759e9u9e/f//o8xTRoXlNT+XjNGClBVqlRJEJD0Jqcud94bJAEJACIvIGnfPDVgUOm11m5rPZI2FFfLb22V8swzzwSbOUyZMsWt86aLXcpASAIQsbwZo2uuucYtqtVu6F5Q0hlBlUtoFkktXZs0aeLe3ER7iM2cOdO1fPUeAwAQORSQPvroI1eSrYY+KsmeOHGi3XfffW5fSQWlli1bupNtXbp0cftM6j1Fs0pIGQhJACJ6sa1mg9SFaOHChW6WaMCAAe7NTLTAVotrs2XL5jb+89bkqXxizZo11qxZM9p8A0AE0nvDc8895y5q2KBGPjq5dt1111nBggXdfTp16mQNGjRwJ9boCJzysCYJQMQGpNWrV1uNGjVcS9bOnTu7hgx6Q1MDBl2ncKSOju+8847bGf3666+3Xbt2uUD1xRdf0MUOACI4JOn94ssvv7SDBw9a9erV3efaCkLU2Ecl2qLbc+XKFeIR40IxkwQgIgOSZoIUkFQGoYCk+vLbbrvNPvvsM7f5n1p9Hzt2zHUkUle7K6+80pVQaCHu4sWLCUgAEIH75qlJj/f5H3/8YTNmzHBrVxs2bGhjx451t23cuNFtLK6gJASklOl/23AAQAQFpLVr17qzfl27drXBgwe721QKoX2Oateu7WaUFJhEb3T16tVzFwBA5DZp0GbhK1ascKXYMTEx1rx5c3ei7cYbb0zQ9XTy5Mm2d+9ed1INKRchCUDEUEBS5yG18tZZPy8geeuMtAO69kaqW7euffrpp65bUfr06a13797Bxbb+lq8AgNRP/8//4IMPXBc7hSJVFSgk3XXXXW5d69atW23ChAluc1hVGqj76TfffGOFCxcO9dDxLxCSAEQUlUqULFnSldJ99913wTVJL7/8stvXokKFCu4+mjlSUKpfv74LSlqbpDbfBCQAiCyrVq2yxx57zIYNG+a2i/BonWrPnj3tww8/tD59+ljx4sXd3nqacbrqqqtCOmb8ezRuABBxVC+us4HqVqc3tFmzZtnbb79tt956a4LZoqNHj9qmTZtcSCpXrlyohw0ACIE333zTtffWnkhq93365rGyf/9+t/+Rrvfug5SNxg0AIo7qxDVzpLasCkdq0qCApHDkBSRtAKg9LUqVKkVAAoAIdujQITt8+HCCffG8gKSyOnW6y5s3rzvxlilTphCOFEmJkAQgImn383Hjxtl//vMfW7BggX377bcuHOnSr18/V1738ccfW5YsWUI9VADAJe5it2XLluB1OlmmtUcKRH4KTe+//757r/C+jpLs1IOQBCBi6Y1PmwDqzU1NHFR3rppztfxWTXnlypVDPUQAwCXiVRIo9DRq1MjGjBnjrr/zzjutTZs2du+997rb1LlO7b+feuope/fdd11XVMJR6sOaJAART2uUunfvbsuWLXOb/sXGxlqlSpUi/nkBgEjgbQ8hWqOqMKSTZQo/3npUrVFVabZafatrnfY+UliaPXs2++alUoQkADCzDRs2WK9evez55593He4AAKmbKgaqVq0aXF+0b98+u+OOO9w+SNpH7+TJk27tqjYZVyc7da9Tafbvv//uup3qa73tIZD60AIcAMzsiiuucLXl6mQHAEjdtOWDNn197733LE+ePO46BSLtgaSmPQpIKsNWR7vVq1e7jnUzZ85061gRGViTBAD/h4AEAKmb16GucePGLigpIG3fvt2FomLFitmNN95oDz74oBUpUsSFo7vvvtuOHDnibtMmsYgczCQBAAAgYtYebd682davX28NGjRwXevuv/9+a9WqlT3++ONuewhtCaHQpIDktfRWtQGldZGFkAQAAIBUTwFJ5XRaS5Q/f343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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "category_stock = df.groupby(\"category\")[\"quantity\"].sum().sort_values()\n", + "plt.figure(figsize=(10,5))\n", + "category_stock.plot(kind=\"bar\")\n", + "plt.title(\"Total Stock by Category\")\n", + "plt.xlabel(\"Category\")\n", + "plt.ylabel(\"Total Quantity\")\n", + "plt.xticks(rotation=45)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3237d30c", + "metadata": {}, + "source": [ + "## Total Stock by Category using Seaborn\n", + "\n", + "This visualizes the same stock information using seaborn." + ] + }, + { + "cell_type": "code", + "execution_count": 150, + "id": "2844664a", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0kAAAIXCAYAAABJihVzAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAVQBJREFUeJzt3QmcjeX///HP2Ma+77ukrEVk/6YsSSiRpRShzb6EUiEikl2hJEvlq1SUCpVKyVhSSpuQUPYsU2T5mfN/vK/f7z7/eywZmplzZs7r+XiczJxz5sw1Z+Z07vd9fa7PFRUIBAIGAAAAAHDS/O8/AAAAAABCEgAAAACcgZkkAAAAAPAhJAEAAACADyEJAAAAAHwISQAAAADgQ0gCAAAAAB9CEgAAAAD4EJIAAAAAwIeQBAAR5NNPP7WoqCj3b0oc9xtvvJGsXwsAiEyEJABIYjpAT8glIcHlqaeeskWLFiXL72zjxo12++23W4kSJSxjxoxWpEgRa9SokU2ZMiVkY0oJNmzYYHfddZcVK1bMoqOjLXfu3NawYUObNWuWnT59+qIfj+cXAJJfuhB8TwCIKC+//HK8z+fOnWsffvjhWdeXK1cuQQfMCi4tWrSwpLRq1Sq74YYbrHjx4nbfffdZwYIFbefOnbZ69WqbNGmS9ezZM9nHlBK8+OKL9uCDD1qBAgXs7rvvtjJlytiff/5py5cvty5dutju3bvt0UcfvajH5PkFgORHSAKAJKZZBT8FDYWkM68PJyNHjrQcOXLYunXrLGfOnPFu27dvX8jGFc70e1VAqlWrlr3//vuWLVu24G19+vSxL7/80r777jtLrY4ePWpZsmQJ9TAAIFFQbgcAYXKA+dBDDwVLtK688kobO3asBQKB4H1Ukqf7zZkzJ1iid88997jbtm/fbt26dXNflylTJsuTJ4+1bt3afv3110saz9atW61ChQpnBSTJnz9/gsYkX3/9tTVp0sSyZ89uWbNmtQYNGrgwcabDhw9b3759rWTJku7nL1q0qHXo0MEOHDhw3jGeOHHCmjVr5sKcZr4uRKVumsXRrJgO5m+55RY3O+YZOnSopU+f3vbv33/W195///3uuTh+/Ph5H3/YsGHu53/11VfjBSRPtWrV4j03+v3Wrl3b/a70O6tatepZ66Yu9Pz+/vvv1rlzZzdzpedNv7OXXnrprO+tvw/9vPq59fvTc71s2bJzlnkuWLDAjUVjyps3rwvz+j5+GoN+n/o7ufnmm93P2759+3/9HAJAuGAmCQBCTEFIB7CffPKJK8mqXLmyO4AdMGCAOzidMGGCu5/K8+69916rXr26O+CU0qVLu38146Og0K5dOxcwFI6mTZtm119/vf3www+WOXPmixqT1iHFxMS4mY+KFSue937/NKbvv//e/vOf/7iANHDgQHfw/Pzzz7sxrVixwmrUqOHu99dff7n7/fjjj+6A/5prrnHh6J133rHffvvNHaif6e+//7Zbb73Vzc589NFHdu211yZodkyh4OGHH3azYRMnTnRrhbSGSIFA5XHDhw+31157zXr06BH8upMnT7rw0qpVK7c261yOHTvmSuquu+46V6KYECpb1O9d4ULfY/78+S7Yvvvuu9a0adMLPr979+61mjVrup9J482XL58tWbLE/Q3Fxsa62StRyKpfv74r9evdu7cLifPmzXN/b2eaPXu2derUyT2fo0aNct9D4/ziiy9c4PWH5v/5n/+xxo0bW926dV3g09+YZtEu9TkEgLASAAAkq+7du2t6KPj5okWL3OcjRoyId7/bb789EBUVFdiyZUvwuixZsgQ6dux41mMeO3bsrOtiYmLc486dOzd43SeffOKu07//5IMPPgikTZvWXWrVqhUYOHBgYNmyZYGTJ0+edd/zjalFixaBDBkyBLZu3Rq8bteuXYFs2bIFrrvuuuB1Q4YMcWN66623znqMuLi4eONesGBB4M8//wzUq1cvkDdv3sDXX3/9jz+H/2uLFCkSiI2NDV7/+uuvu+snTZoUvE4/a40aNeJ9vcZ1oefsm2++cffp3bt3IKHO/J3pua1YsWKgfv36CXp+u3TpEihUqFDgwIED8a5v165dIEeOHMHHHzdunBub/s48f//9d6Bs2bLxfi59//z587sx6HbPu+++6+6n35NH49F1jzzyyFnjutTnEADCCeV2ABBiWr+SNm1a69WrV7zrVX6nWSbNDlyIZkI8p06dsj/++MMuv/xyd+b/q6++uugxqYudZpI00/HNN9/YmDFj3KyBOtxphichpW0ffPCBa+Zw2WWXBa8vVKiQ3XnnnbZy5Uo32yFvvvmmXX311Xbbbbed9TiaJfE7cuSI3XjjjfbTTz+5MjHNuiWUyvf8ZXBqNqHx6Pn332fNmjWujMyj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/db69u3rDvzuu+8+dyAoWmeh+xw8eNDGjx/v1iIBkUKzQ2pk8sQTT5x1kmDBggWuxXfWrFnd4tszy1IBAJG1jYqOofS5li/oZNtXX33lKnW016TK6/x7jbF0IXSYSUKQpnjl+PHjwev04syZM6d7weqFfNVVV7kQpMW5alPpzSgpOKn19zvvvENAQsTQ60OvBV20p8WSJUvsm2++ibfnhdrkq42ryvJUZqfXEQAgcniB57333nOdgOvVq2e1a9cOnoC+9tprXbm2mjY0btzYvbd461xZ2x06hCT8/z+GNGlc60ktJP/www/ddTqg83dRUZC6+uqrXatXHRjqzLj2gpEsWbK4+logtfNCkDo9qgxC7b41k6SmDA888ICbcfUHpdtvv90mTpzoSvK8unMAQGScfFbg0T557dq1c2tW9V6gipubb745eLJZQUmVB+qOqjJuhB4hCfGoFeVvv/3myuc0/etv56qDOwUpr45WtbI6O64zI+rAAkTSGUG9sd16662ui51OLuj1odeMwpI6PmpGSZ588knr16+fNWzY0EqWLBnq4QMAkpg6m4p3zKRjJe0vqfcLrd++/PLL3ZrvvHnzujWtCkdSrVo1t6+kTqoh9FiThLOovbfKgjQzpKlfHRRq0zK92HVRkNJBosqL1P1OjRy8Dc6ASKCZVpVMaDNlBSW1yNesq4LS4cOHrXr16u5jLcaNiYlxb3p68wMApG6vvfaajRkzxvr37++a+cjWrVtd1U3Pnj1dA6wbbrjB6tat6zaIVUm2Zo90ctq7P8IDM0k4i85wTJo0yf7++29XTqSglDlzZldKpDPmWrOkjnd60WujWQISIoVOGKg7nc76qXGJLjoT6C+t0Bo+veEpPFWpUsXWrFlDQAKACKEyOi1TmDVrVnA5gvbKu/vuu12jBpXaad9JzRZlzJjRNWtQ2bbWduvYCuGDnT5xTldeeaU7S96nTx9XctetWzcaMiDiaQY1ffr0rpTC29/Ia9fqrTXauXOnm0FSZ0hd75VbAABSL605UnMrtezWDJGOn9TgSu8DmiFSS281ZPj555+tVKlSruOp6L3jjTfecPshKUQhfPDujX+cUVInOx3kaUfozz//PN7t/oXpQKTQGT+FILW711k/zR4pPOn1oID09NNPu5JVXUdAAoDUb/369W6t0WOPPebWdV9xxRWufC46OtotSfBmlNSpTtU3L7/8stsaonPnzm6fJF3nb5KF8EBIwj/SC10vZJ09HzhwoCsd8njtKYHUyjsRoO51CkVqUKLSU236t2jRIleW6pVH6PXwwgsv2MqVK916PgBAZNAs0P333+9OlCko6V9V5HhBSTNKanYlo0ePduXY06dPd0satC+STkoj/NC4AQny008/2eDBg10traaMgUjpYqe9v0aMGOEalqgpw8MPP2z33HOP626nf9XCVR0gdYZQ+yR9+umnbi0SACD1UzWBVzWgMjvNGmkNkt43VHWgIKT1RnoPUZBq27atu+/evXtdyR0n1cIXIQkJ5m8HDkSCZcuWWcuWLd2bnToQaX8wNStZunSpa+mtQPTuu++68jq9Kd57771Wrly5UA8bAJCMtNbI2/RV67nV4U6zQ2cGJd1P65M6derE7ycFICQBgJlry+qd0fM2AOzYsaMVKFDA7W+hOvMGDRrY9ddfb88///xZM05eC3AAQGTw/v9/ZlCaNm2aW3ekLnZeUFLDBoWjfPny2dy5c2nSkAKwJglAxFMnugceeMD27Nnzv/9jTJPGBSW1v9eMkQJUjRo14gUkvcmpy533BklAAoDIC0jaN08NGFR6rbXbWo+kDcXV8ltbpTz++OPBZg5z5sxx67zpYpcyEJIARCxvxujqq692i2q1G7oXlHRGUOUSmkVSS9fbbrvNvbmJ9hBbuHCha/nqPQYAIHIoIL399tuuJFsNfVSSPXPmTLvrrrvcvpIKSu3atXMn23r16uX2mdR7imaVkDIQkgBE9GJbzQapC9GKFSvcLNGwYcPcm5loga0W12bLls1t/OetyVP5xDfffGOtWrWizTcARCC9Nzz55JPuooYNauSjk2vXXHONFSpUyN2nR48e1rRpU3dijY7AKQ9rkgBEbEDasGGD1alTx7Vk7dmzp2vIoDc0NWDQdQpH6uj46quvup3Rr732Wtu9e7cLVB999BFd7AAggkOS3i8+/vhjO3TokNWuXdt9rq0gRI19VKItuj1XrlwhHjEuFjNJACIyIGkmSAFJZRAKSKovv+mmm+yDDz5wm/+p1ffx48ddRyJ1tatYsaIrodBC3FWrVhGQACAC981Tkx7v8z/++MMWLFjg1q42a9bMpk6d6m7bvHmz21hcQUkISCnT/7bhAIAICkjffvutO+vXp08fGzlypLtNpRDa56h+/fpuRkmBSfRG17hxY3cBAERukwZtFr5+/XpXil2wYEFr06aNO9F23XXXxet6Onv2bNu3b587qYaUi5AEIGIoIKnzkFp566yfF5C8dUbaAV17IzVq1Mjef/99160offr09sgjjwQX2/pbvgIAUj/9P//NN990XewUilRVoJB0++23u3Wtv/76q82YMcNtDqtKA3U//eyzz6xIkSKhHjr+BUISgIiiUolSpUq5UrovvvgiuCZp0qRJbl+LChUquPto5khBqUmTJi4oaW2S2nwTkAAgsnz99dfWrVs3GzNmjNsuwqN1qgMGDLC33nrLBg0aZCVKlHB762nGqVKlSiEdM/49GjcAiDiqF9fZQHWr0xvaokWL7JVXXrEbb7wx3mzRsWPHbMuWLS4klStXLtTDBgCEwEsvveTae2tPJLX7PnPzWDlw4IDb/0jXe/dBykbjBgARR3XimjlSW1aFIzVpUEBSOPICkjYA1J4WpUuXJiABQAQ7fPiwHTlyJN6+eF5AUlmdOt3lzZvXnXjLlClTCEeKxERIAhCRtPv5tGnT7D//+Y8tX77cPv/8cxeOdBkyZIgrr3vnnXcsS5YsoR4qACCZu9ht27YteJ1OlmntkQKRn0LTG2+84d4rvK+jJDv1ICQBiFh649MmgHpzUxMH1Z2r5lwtv1VTXq1atVAPEQCQTLxKAoWe5s2b25QpU9z1t956q3Xq1MnuvPNOd5s616n996OPPmqvvfaa64pKOEp9WJMEIOJpjVK/fv1s7dq1btO/mJgYq1q1asQ/LwAQCbztIURrVBWGdLJM4cdbj6o1qirNVqtvda3T3kcKS4sXL2bfvFSKkAQAZrZp0yYbOHCgPfXUU67DHQAgdVPFQM2aNYPri/bv32+33HKL2wdJ++idOnXKrV3VJuPqZKfudSrN/v333123U32ttz0EUh9agAOAmV155ZWutlyd7AAAqZu2fNCmr6+//rrlyZPHXadApD2Q1LRHAUll2Opot2HDBtexbuHChW4dKyIDa5IA4P8QkAAgdfM61LVo0cIFJQWkHTt2uFBUvHhxu+666+yee+6xokWLunDUunVrO3r0qLtNm8QicjCTBAAAgIhZe7R161b76aefrGnTpq5r3d13323t27e33r17u+0htCWEQpMCktfSW9UGlNZFFkISAAAAUj0FJJXTaS1R/vz53QyRZpS0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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(10, 5))\n", + "sns.barplot(x=category_stock.index, y=category_stock.values)\n", + "plt.title(\"Total Stock by Category\")\n", + "plt.xlabel(\"Category\")\n", + "plt.ylabel(\"Total Quantity\")\n", + "plt.xticks(rotation=45)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d8be90cf", + "metadata": {}, + "source": [ + "## Low Stock Products\n", + "\n", + "This identifies products whose quantity is below 5." + ] + }, + { + "cell_type": "code", + "execution_count": 151, + "id": "bb67fc22", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of low-stock products: 1\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
idnamedescriptionpricebrandquantitycategory
118Pancooking pan1000.0prestige4Kitchen
\n", + "
" + ], + "text/plain": [ + " id name description price brand quantity category\n", + "11 8 Pan cooking pan 1000.0 prestige 4 Kitchen" + ] + }, + "execution_count": 151, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "low_stock_rows = []\n", + "for index,row in df.iterrows():\n", + " if row[\"quantity\"]<5:\n", + " low_stock_rows.append(row)\n", + "\n", + "low_stock_df = pd.DataFrame(low_stock_rows)\n", + "print(\"Number of low-stock products:\", len(low_stock_df))\n", + "low_stock_df" + ] + }, + { + "cell_type": "markdown", + "id": "46cb042b", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This notebook demonstrates:\n", + "\n", + "- raw MongoEngine queries using `Product.objects()` and `ProductCategory.objects()`\n", + "- conversion of MongoDB documents into a pandas DataFrame\n", + "- category-wise inventory analysis\n", + "- stock visualization using matplotlib and seaborn\n", + "- identification of low-stock products" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.12.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 7b5d733b78916e5fd2abbb94c61b7080416f6127 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 02:27:26 +0530 Subject: [PATCH 54/83] week5 - completed --- .../notebook/week5_inventory_analysis.ipynb | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/backend/python/week5/notebook/week5_inventory_analysis.ipynb b/backend/python/week5/notebook/week5_inventory_analysis.ipynb index eb4b88a1c..63f5b7c1d 100644 --- a/backend/python/week5/notebook/week5_inventory_analysis.ipynb +++ b/backend/python/week5/notebook/week5_inventory_analysis.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 140, + "execution_count": 152, "id": "1975c454", "metadata": {}, "outputs": [ @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 141, + "execution_count": 153, "id": "4175ca63", "metadata": {}, "outputs": [], @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 142, + "execution_count": 154, "id": "c645f265", "metadata": {}, "outputs": [ @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 143, + "execution_count": 155, "id": "2e158954", "metadata": {}, "outputs": [ @@ -94,12 +94,12 @@ "{'id': 3, 'name': 'Rice', 'description': 'Basmati rice', 'price': '400.00', 'brand': 'India Gate', 'quantity': 10, 'category': {'id': 1, 'title': 'Food', 'description': 'Daily grocery items', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000)}, 'category_id': 1, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 402000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 402000)}\n", "{'id': 5, 'name': 'Pan', 'description': 'Non-stick pan', 'price': '999.00', 'brand': 'Prestige', 'quantity': 5, 'category': {'id': 2, 'title': 'Electronics', 'description': 'Electronic products', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 395000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 394000)}, 'category_id': 2, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 420000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 420000)}\n", "{'id': 6, 'name': 'Soap', 'description': 'Bath soap', 'price': '45.00', 'brand': 'Dove', 'quantity': 20, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 430000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 430000)}\n", - "{'id': ObjectId('69c76070d11850d8ae737d65'), 'name': 'Test Product 1', 'description': 'No category no brand', 'price': '100.00', 'brand': 'Unknown', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 363000)}\n", - "{'id': ObjectId('69c76078d11850d8ae737d67'), 'name': 'Test Product 2', 'description': 'No category but has brand', 'price': '200.00', 'brand': 'Nike', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 365000)}\n", - "{'id': ObjectId('69c7607ed11850d8ae737d69'), 'name': 'Test Product 3', 'description': 'Has category but no brand', 'price': '150.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 368000)}\n", - "{'id': ObjectId('69c7728dd11850d8ae737d6c'), 'name': 'Legacy Product 1', 'description': 'Missing category only', 'price': '100.00', 'brand': 'Nike', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719005), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 669000)}\n", - "{'id': ObjectId('69c77295d11850d8ae737d6e'), 'name': 'Legacy Product 2', 'description': 'Missing brand only', 'price': '120.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719613), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 673000)}\n", - "{'id': ObjectId('69c772a8d11850d8ae737d70'), 'name': 'Legacy Product 3', 'description': 'Missing both brand and category', 'price': '150.00', 'brand': 'Unknown', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 52, 45, 719613), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 675000)}\n" + "{'id': ObjectId('69c76070d11850d8ae737d65'), 'name': 'Test Product 1', 'description': 'No category no brand', 'price': '100.00', 'brand': 'Unknown', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 363000)}\n", + "{'id': ObjectId('69c76078d11850d8ae737d67'), 'name': 'Test Product 2', 'description': 'No category but has brand', 'price': '200.00', 'brand': 'Nike', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 365000)}\n", + "{'id': ObjectId('69c7607ed11850d8ae737d69'), 'name': 'Test Product 3', 'description': 'Has category but no brand', 'price': '150.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 368000)}\n", + "{'id': ObjectId('69c7728dd11850d8ae737d6c'), 'name': 'Legacy Product 1', 'description': 'Missing category only', 'price': '100.00', 'brand': 'Nike', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 669000)}\n", + "{'id': ObjectId('69c77295d11850d8ae737d6e'), 'name': 'Legacy Product 2', 'description': 'Missing brand only', 'price': '120.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 673000)}\n", + "{'id': ObjectId('69c772a8d11850d8ae737d70'), 'name': 'Legacy Product 3', 'description': 'Missing both brand and category', 'price': '150.00', 'brand': 'Unknown', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 675000)}\n" ] } ], @@ -122,7 +122,7 @@ }, { "cell_type": "code", - "execution_count": 144, + "execution_count": 156, "id": "bb270c30", "metadata": {}, "outputs": [ @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 145, + "execution_count": 157, "id": "05aab583", "metadata": {}, "outputs": [ @@ -361,7 +361,7 @@ "11 Kitchen " ] }, - "execution_count": 145, + "execution_count": 157, "metadata": {}, "output_type": "execute_result" } @@ -394,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": 146, + "execution_count": 158, "id": "9f321f59", "metadata": {}, "outputs": [ @@ -425,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 147, + "execution_count": 159, "id": "df40dff6", "metadata": {}, "outputs": [ @@ -589,7 +589,7 @@ "max NaN NaN NaN 1000.000000 NaN 20.000000 NaN" ] }, - "execution_count": 147, + "execution_count": 159, "metadata": {}, "output_type": "execute_result" } @@ -610,7 +610,7 @@ }, { "cell_type": "code", - "execution_count": 148, + "execution_count": 160, "id": "633db4db", "metadata": {}, "outputs": [ @@ -625,7 +625,7 @@ "Name: count, dtype: int64" ] }, - "execution_count": 148, + "execution_count": 160, "metadata": {}, "output_type": "execute_result" } @@ -646,7 +646,7 @@ }, { "cell_type": "code", - "execution_count": 149, + "execution_count": 161, "id": "ee3ecf73", "metadata": {}, "outputs": [ @@ -684,7 +684,7 @@ }, { "cell_type": "code", - "execution_count": 150, + "execution_count": 162, "id": "2844664a", "metadata": {}, "outputs": [ @@ -721,7 +721,7 @@ }, { "cell_type": "code", - "execution_count": 151, + "execution_count": 163, "id": "bb67fc22", "metadata": {}, "outputs": [ @@ -782,7 +782,7 @@ "11 8 Pan cooking pan 1000.0 prestige 4 Kitchen" ] }, - "execution_count": 151, + "execution_count": 163, "metadata": {}, "output_type": "execute_result" } From 5c29375be180d5bec907cd578a981698789f8566 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 02:28:55 +0530 Subject: [PATCH 55/83] readme updated --- backend/python/README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/backend/python/README.md b/backend/python/README.md index bdc52334b..71a5ff227 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -454,6 +454,27 @@ This runs automatically — no manual step needed. The process is idempotent: al --- +# Week 5 - Interactive Data Tools + +## Overview + +This week focuses on building a Streamlit dashboard and using Jupyter Notebook for data exploration. + +## Features + +- Display inventory in a table format using Streamlit +- Add and remove products directly from UI +- Sidebar filter by product category +- Stock alert for low-quantity items +- Jupyter Notebook for MongoEngine queries and data visualization + +## Files + +- `dashboard.py` → Streamlit inventory dashboard +- `notebook/week5_inventory_analysis.ipynb` → Data analysis and visualization + +--- + # Interneers Lab - Backend in Python Welcome to the **Interneers Lab 2026** Python backend! This serves as a minimal starter kit for learning and experimenting with: From 7fe0051811440463b26fd36f204396ca7fce3b12 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 02:29:53 +0530 Subject: [PATCH 56/83] readme updated --- backend/python/README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index 71a5ff227..6754d510b 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -456,10 +456,6 @@ This runs automatically — no manual step needed. The process is idempotent: al # Week 5 - Interactive Data Tools -## Overview - -This week focuses on building a Streamlit dashboard and using Jupyter Notebook for data exploration. - ## Features - Display inventory in a table format using Streamlit From d4f68bc5a9cb5b74527f1d8584a0c0d6530a8984 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 03:21:13 +0530 Subject: [PATCH 57/83] week6 - api key added --- .../notebook/week5_inventory_analysis.ipynb | 44 +++++++++---------- backend/python/week6/test_gemini.py | 14 ++++++ 2 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 backend/python/week6/test_gemini.py diff --git a/backend/python/week5/notebook/week5_inventory_analysis.ipynb b/backend/python/week5/notebook/week5_inventory_analysis.ipynb index 63f5b7c1d..e45016be9 100644 --- a/backend/python/week5/notebook/week5_inventory_analysis.ipynb +++ b/backend/python/week5/notebook/week5_inventory_analysis.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 152, + "execution_count": 164, "id": "1975c454", "metadata": {}, "outputs": [ @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 153, + "execution_count": 165, "id": "4175ca63", "metadata": {}, "outputs": [], @@ -52,7 +52,7 @@ }, { "cell_type": "code", - "execution_count": 154, + "execution_count": 166, "id": "c645f265", "metadata": {}, "outputs": [ @@ -81,7 +81,7 @@ }, { "cell_type": "code", - "execution_count": 155, + "execution_count": 167, "id": "2e158954", "metadata": {}, "outputs": [ @@ -94,12 +94,12 @@ "{'id': 3, 'name': 'Rice', 'description': 'Basmati rice', 'price': '400.00', 'brand': 'India Gate', 'quantity': 10, 'category': {'id': 1, 'title': 'Food', 'description': 'Daily grocery items', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 16, 429000)}, 'category_id': 1, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 402000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 402000)}\n", "{'id': 5, 'name': 'Pan', 'description': 'Non-stick pan', 'price': '999.00', 'brand': 'Prestige', 'quantity': 5, 'category': {'id': 2, 'title': 'Electronics', 'description': 'Electronic products', 'created_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 395000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 54, 27, 394000)}, 'category_id': 2, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 420000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 420000)}\n", "{'id': 6, 'name': 'Soap', 'description': 'Bath soap', 'price': '45.00', 'brand': 'Dove', 'quantity': 20, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 430000), 'updated_at': datetime.datetime(2026, 3, 22, 22, 7, 6, 430000)}\n", - "{'id': ObjectId('69c76070d11850d8ae737d65'), 'name': 'Test Product 1', 'description': 'No category no brand', 'price': '100.00', 'brand': 'Unknown', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 363000)}\n", - "{'id': ObjectId('69c76078d11850d8ae737d67'), 'name': 'Test Product 2', 'description': 'No category but has brand', 'price': '200.00', 'brand': 'Nike', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 365000)}\n", - "{'id': ObjectId('69c7607ed11850d8ae737d69'), 'name': 'Test Product 3', 'description': 'Has category but no brand', 'price': '150.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 368000)}\n", - "{'id': ObjectId('69c7728dd11850d8ae737d6c'), 'name': 'Legacy Product 1', 'description': 'Missing category only', 'price': '100.00', 'brand': 'Nike', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 669000)}\n", - "{'id': ObjectId('69c77295d11850d8ae737d6e'), 'name': 'Legacy Product 2', 'description': 'Missing brand only', 'price': '120.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 673000)}\n", - "{'id': ObjectId('69c772a8d11850d8ae737d70'), 'name': 'Legacy Product 3', 'description': 'Missing both brand and category', 'price': '150.00', 'brand': 'Unknown', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 53, 55, 904034), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 675000)}\n" + "{'id': ObjectId('69c76070d11850d8ae737d65'), 'name': 'Test Product 1', 'description': 'No category no brand', 'price': '100.00', 'brand': 'Unknown', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 56, 58, 433837), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 363000)}\n", + "{'id': ObjectId('69c76078d11850d8ae737d67'), 'name': 'Test Product 2', 'description': 'No category but has brand', 'price': '200.00', 'brand': 'Nike', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 56, 58, 433837), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 365000)}\n", + "{'id': ObjectId('69c7607ed11850d8ae737d69'), 'name': 'Test Product 3', 'description': 'Has category but no brand', 'price': '150.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 56, 58, 434465), 'updated_at': datetime.datetime(2026, 3, 28, 5, 1, 47, 368000)}\n", + "{'id': ObjectId('69c7728dd11850d8ae737d6c'), 'name': 'Legacy Product 1', 'description': 'Missing category only', 'price': '100.00', 'brand': 'Nike', 'quantity': 5, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 56, 58, 434465), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 669000)}\n", + "{'id': ObjectId('69c77295d11850d8ae737d6e'), 'name': 'Legacy Product 2', 'description': 'Missing brand only', 'price': '120.00', 'brand': 'Unknown', 'quantity': 8, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 56, 58, 434465), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 673000)}\n", + "{'id': ObjectId('69c772a8d11850d8ae737d70'), 'name': 'Legacy Product 3', 'description': 'Missing both brand and category', 'price': '150.00', 'brand': 'Unknown', 'quantity': 10, 'category': {'id': 3, 'title': 'Miscellaneous', 'description': 'Default category for uncategorized products', 'created_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 835000), 'updated_at': datetime.datetime(2026, 3, 22, 21, 56, 22, 834000)}, 'category_id': 3, 'created_at': datetime.datetime(2026, 3, 30, 20, 56, 58, 434465), 'updated_at': datetime.datetime(2026, 3, 28, 6, 19, 39, 675000)}\n" ] } ], @@ -122,7 +122,7 @@ }, { "cell_type": "code", - "execution_count": 156, + "execution_count": 168, "id": "bb270c30", "metadata": {}, "outputs": [ @@ -158,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 157, + "execution_count": 169, "id": "05aab583", "metadata": {}, "outputs": [ @@ -361,7 +361,7 @@ "11 Kitchen " ] }, - "execution_count": 157, + "execution_count": 169, "metadata": {}, "output_type": "execute_result" } @@ -394,7 +394,7 @@ }, { "cell_type": "code", - "execution_count": 158, + "execution_count": 170, "id": "9f321f59", "metadata": {}, "outputs": [ @@ -425,7 +425,7 @@ }, { "cell_type": "code", - "execution_count": 159, + "execution_count": 171, "id": "df40dff6", "metadata": {}, "outputs": [ @@ -589,7 +589,7 @@ "max NaN NaN NaN 1000.000000 NaN 20.000000 NaN" ] }, - "execution_count": 159, + "execution_count": 171, "metadata": {}, "output_type": "execute_result" } @@ -610,7 +610,7 @@ }, { "cell_type": "code", - "execution_count": 160, + "execution_count": 172, "id": "633db4db", "metadata": {}, "outputs": [ @@ -625,7 +625,7 @@ "Name: count, dtype: int64" ] }, - "execution_count": 160, + "execution_count": 172, "metadata": {}, "output_type": "execute_result" } @@ -646,7 +646,7 @@ }, { "cell_type": "code", - "execution_count": 161, + "execution_count": 173, "id": "ee3ecf73", "metadata": {}, "outputs": [ @@ -684,7 +684,7 @@ }, { "cell_type": "code", - "execution_count": 162, + "execution_count": 174, "id": "2844664a", "metadata": {}, "outputs": [ @@ -721,7 +721,7 @@ }, { "cell_type": "code", - "execution_count": 163, + "execution_count": 175, "id": "bb67fc22", "metadata": {}, "outputs": [ @@ -782,7 +782,7 @@ "11 8 Pan cooking pan 1000.0 prestige 4 Kitchen" ] }, - "execution_count": 163, + "execution_count": 175, "metadata": {}, "output_type": "execute_result" } diff --git a/backend/python/week6/test_gemini.py b/backend/python/week6/test_gemini.py new file mode 100644 index 000000000..ee3327d82 --- /dev/null +++ b/backend/python/week6/test_gemini.py @@ -0,0 +1,14 @@ +import os +from dotenv import load_dotenv +from google import genai + +load_dotenv() + +client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) + +response = client.models.generate_content( + model="gemini-2.5-flash", + contents="Say hello in one short line." +) + +print(response.text) From 6523706d4d33499d224a26618278ba7121546c04 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 16:46:08 +0530 Subject: [PATCH 58/83] week6 - task1 completed --- backend/python/week6/__init__.py | 0 backend/python/week6/task_1.py | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 backend/python/week6/__init__.py create mode 100644 backend/python/week6/task_1.py diff --git a/backend/python/week6/__init__.py b/backend/python/week6/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week6/task_1.py b/backend/python/week6/task_1.py new file mode 100644 index 000000000..d9a602abd --- /dev/null +++ b/backend/python/week6/task_1.py @@ -0,0 +1,66 @@ +import os +from dotenv import load_dotenv +from google import genai + +load_dotenv() + +client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) + + +# official gemini 2.5 flash prices +INPUT_PRICE_PER_1M = 0.30 +OUTPUT_PRICE_PER_1M = 2.50 + + +def calculate_cost(prompt_tokens, total_tokens): + output_tokens = total_tokens - prompt_tokens + inp_cost = (prompt_tokens/1000000)*INPUT_PRICE_PER_1M + out_cost = (output_tokens/1000000)*OUTPUT_PRICE_PER_1M + tot_cost = inp_cost + out_cost + return inp_cost, out_cost, tot_cost, output_tokens + + +def run_prompt(temperature): + response = client.models.generate_content( + model="gemini-2.5-flash", + contents="Generate 5 product names for a toy store. Return only a numbered list.", + # extra settings for temperature + config={ + "temperature": temperature + } + ) + print(f"\n===TEMPERATURE: {temperature}===") + print("\nTEXT RESPONSE:") + print(response.text) + print("\nFULL RESPONSE:") + print(response) + # metadata : data about data + usage = response.usage_metadata + # number of tokens in input prompt + prompt_tokens = usage.prompt_token_count + # total tokens used in whole request + total_tokens = usage.total_token_count + # number of tokens in visible response text + candidate_tokens = usage.candidates_token_count + # number of tokens used in thinking and internal reasoning + thoughts_tokens = usage.thoughts_token_count + print("\nTOKEN USAGE:") + print("Prompt tokens:", prompt_tokens) + print("Candidate tokens:", candidate_tokens) + print("Thoughts tokens:", thoughts_tokens) + print("Total tokens:", total_tokens) + inp_cost, out_cost, tot_cost, output_tokens = calculate_cost( + prompt_tokens, total_tokens) + print("\nCOST BREAKDOWN:") + print("Output tokens:", output_tokens) + print(f"Input cost: ${inp_cost:.8f}") + print(f"Output cost: ${out_cost:.8f}") + print(f"Total cost: ${tot_cost:.8f}") + + +# temperature controls randomness in token selection +# low temperature => less randomness => more stable outcomes +run_prompt(0) +run_prompt(0.5) +run_prompt(1) +run_prompt(1.5) From 5ec59953a029d5a14fd0760462181243a6757ea6 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 16:50:51 +0530 Subject: [PATCH 59/83] readme updated --- backend/python/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/python/README.md b/backend/python/README.md index 6754d510b..5d4168deb 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -471,7 +471,7 @@ This runs automatically — no manual step needed. The process is idempotent: al --- -# Interneers Lab - Backend in Python + From 98da952c0777982ae3d71c3305173cbcc711f922 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 19:02:31 +0530 Subject: [PATCH 60/83] week6 - schema added --- backend/python/week6/schema.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 backend/python/week6/schema.py diff --git a/backend/python/week6/schema.py b/backend/python/week6/schema.py new file mode 100644 index 000000000..6e87621a6 --- /dev/null +++ b/backend/python/week6/schema.py @@ -0,0 +1,28 @@ +from typing import Annotated, List, Optional +from pydantic import BaseModel, Field + + +class ProductSchema(BaseModel): + name: Annotated[str, Field(min_length=1, max_length=200)] + description: Annotated[str, Field(min_length=1, max_length=500)] + category: Annotated[str, Field(min_length=1, max_length=100)] + brand: Annotated[str, Field(min_length=1, max_length=100)] + price: Annotated[float, Field(gt=0)] + quantity: Annotated[int, Field(ge=0)] + + +class ProductListSchema(BaseModel): + products: List[ProductSchema] + + +class FutureStockEventSchema(BaseModel): + title: Annotated[str, Field(min_length=3, max_length=120)] + event_type: Annotated[str, Field(min_length=3, max_length=50)] + expected_date: str + product_name: Annotated[str, Field(min_length=1, max_length=100)] + quantity_change: int + note: Optional[str] = None + + +class FutureStockEventListSchema(BaseModel): + events: List[FutureStockEventSchema] From f9b93363a8815a26e4f48edbc6378c01a446f58d Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 22:38:50 +0530 Subject: [PATCH 61/83] week6 - task 2 and 3 completed --- backend/python/week6/generated_products.json | 404 ++++++++++++++++++ backend/python/week6/schema.py | 2 +- backend/python/week6/synthetic_products.ipynb | 340 +++++++++++++++ backend/python/week6/task_1.py | 10 +- backend/python/week6/task_2.py | 147 +++++++ backend/python/week6/task_4.py | 0 .../week6/week6/generated_products.json | 44 ++ 7 files changed, 941 insertions(+), 6 deletions(-) create mode 100644 backend/python/week6/generated_products.json create mode 100644 backend/python/week6/synthetic_products.ipynb create mode 100644 backend/python/week6/task_2.py create mode 100644 backend/python/week6/task_4.py create mode 100644 backend/python/week6/week6/generated_products.json diff --git a/backend/python/week6/generated_products.json b/backend/python/week6/generated_products.json new file mode 100644 index 000000000..feb73bcd6 --- /dev/null +++ b/backend/python/week6/generated_products.json @@ -0,0 +1,404 @@ +{ + "products": [ + { + "name": "Deluxe City Building Blocks Set", + "description": "A large set of colorful interlocking blocks for creative construction, perfect for developing motor skills and imagination.", + "category": "building blocks", + "brand": "BlockMaster", + "price": 49.99, + "quantity": 75 + }, + { + "name": "Mini Robot Construction Kit", + "description": "Build your own small, functional robot with this engaging construction kit, featuring easy-to-follow instructions.", + "category": "building blocks", + "brand": "RoboBuild", + "price": 29.5, + "quantity": 120 + }, + { + "name": "Wooden Farm Animal Blocks", + "description": "Chunky wooden blocks shaped like farm animals, ideal for toddlers to stack, sort, and play.", + "category": "building blocks", + "brand": "EcoPlay", + "price": 22.75, + "quantity": 90 + }, + { + "name": "Magnetic Tile Creativity Set", + "description": "Geometric magnetic tiles that connect to build 2D and 3D structures, fostering STEM learning.", + "category": "building blocks", + "brand": "MagnaTiles", + "price": 65.0, + "quantity": 60 + }, + { + "name": "Interlocking Gear System", + "description": "A set of colorful gears and connectors to create intricate moving machines, promoting understanding of mechanics.", + "category": "building blocks", + "brand": "GearWorks", + "price": 35.25, + "quantity": 85 + }, + { + "name": "Princess Royal Doll", + "description": "A beautiful doll with a shimmering gown and accessories, ready for imaginative royal adventures.", + "category": "dolls", + "brand": "Dreamland Dolls", + "price": 18.99, + "quantity": 150 + }, + { + "name": "Doctor Play Doll Set", + "description": "An articulated doll dressed as a doctor, complete with medical tools for role-playing and learning.", + "category": "dolls", + "brand": "Career Dolls", + "price": 24.5, + "quantity": 110 + }, + { + "name": "Baby Cuddle Doll", + "description": "A soft-bodied baby doll perfect for cuddling and nurturing play, comes with a pacifier and blanket.", + "category": "dolls", + "brand": "Sweet Dreams Babies", + "price": 15.75, + "quantity": 130 + }, + { + "name": "Fashionista Doll Collection", + "description": "A stylish doll with multiple outfits and accessories for endless fashion fun and creative styling.", + "category": "dolls", + "brand": "Glamour Girls", + "price": 29.0, + "quantity": 95 + }, + { + "name": "Fantasy Fairy Doll", + "description": "A magical fairy doll with sparkling wings and a whimsical outfit, inspiring enchanting stories.", + "category": "dolls", + "brand": "Enchanted Toys", + "price": 21.25, + "quantity": 105 + }, + { + "name": "Superhero Action Figure", + "description": "Highly detailed action figure of a popular superhero, featuring multiple points of articulation and accessories.", + "category": "action figures", + "brand": "Heroic Legends", + "price": 14.99, + "quantity": 180 + }, + { + "name": "Space Explorer Figure", + "description": "An astronaut action figure with a removable helmet and space tools, perfect for cosmic adventures.", + "category": "action figures", + "brand": "Galaxy Quest", + "price": 12.5, + "quantity": 160 + }, + { + "name": "Mythical Beast Figure", + "description": "A fearsome mythical creature action figure with intricate sculpting and dynamic pose.", + "category": "action figures", + "brand": "Fantasy Fighters", + "price": 17.0, + "quantity": 140 + }, + { + "name": "Ninja Warrior Figure", + "description": "Agile ninja action figure with authentic weapons and martial arts poses for epic battles.", + "category": "action figures", + "brand": "Shadow Strike", + "price": 13.75, + "quantity": 170 + }, + { + "name": "Robot Defender Figure", + "description": "A futuristic robot action figure with light-up features and interchangeable weapon attachments.", + "category": "action figures", + "brand": "Mech Warriors", + "price": 19.25, + "quantity": 125 + }, + { + "name": "Classic Family Board Game", + "description": "A timeless board game for 2-4 players, involving strategy and luck, fun for all ages.", + "category": "board games", + "brand": "GameNight Classics", + "price": 25.99, + "quantity": 100 + }, + { + "name": "Mystery Detective Game", + "description": "Solve a thrilling mystery by gathering clues and interrogating suspects in this engaging board game.", + "category": "board games", + "brand": "Whodunit Games", + "price": 32.0, + "quantity": 80 + }, + { + "name": "Fantasy Adventure Board Game", + "description": "Embark on an epic quest in a magical land, battling monsters and collecting treasures.", + "category": "board games", + "brand": "Epic Journeys", + "price": 45.5, + "quantity": 55 + }, + { + "name": "Strategy City Builder Game", + "description": "Build and manage your own city, making strategic decisions to grow your empire.", + "category": "board games", + "brand": "Urban Planners", + "price": 38.75, + "quantity": 70 + }, + { + "name": "Cooperative Storytelling Game", + "description": "Work together with friends to create a unique story, fostering creativity and teamwork.", + "category": "board games", + "brand": "Narrative Play", + "price": 28.0, + "quantity": 90 + }, + { + "name": "Animal Kingdom Puzzle (1000 pieces)", + "description": "A challenging 1000-piece jigsaw puzzle featuring a vibrant illustration of various animals in their habitat.", + "category": "puzzles", + "brand": "PuzzleMania", + "price": 19.99, + "quantity": 110 + }, + { + "name": "World Map Floor Puzzle (50 pieces)", + "description": "A large, durable floor puzzle depicting a colorful world map, perfect for young learners.", + "category": "puzzles", + "brand": "GeoPuzzles", + "price": 15.5, + "quantity": 130 + }, + { + "name": "3D Eiffel Tower Puzzle", + "description": "Construct a detailed 3D model of the Eiffel Tower using interlocking plastic pieces.", + "category": "puzzles", + "brand": "Structure Puzzles", + "price": 27.0, + "quantity": 80 + }, + { + "name": "Alphabet Learning Puzzle", + "description": "A wooden peg puzzle designed to help toddlers learn letters and improve fine motor skills.", + "category": "puzzles", + "brand": "SmartStart Toys", + "price": 12.25, + "quantity": 150 + }, + { + "name": "Space Exploration Jigsaw (500 pieces)", + "description": "A captivating 500-piece puzzle showcasing astronauts and celestial bodies in outer space.", + "category": "puzzles", + "brand": "Cosmic Puzzles", + "price": 16.75, + "quantity": 120 + }, + { + "name": "Kids First Microscope Kit", + "description": "An easy-to-use microscope designed for children to explore the micro-world, includes slides and tools.", + "category": "educational toys", + "brand": "Science Explorers", + "price": 39.99, + "quantity": 70 + }, + { + "name": "Coding Robot for Kids", + "description": "Introduce basic coding concepts with this programmable robot, featuring interactive challenges.", + "category": "educational toys", + "brand": "Code & Play", + "price": 55.0, + "quantity": 45 + }, + { + "name": "Human Anatomy Model Kit", + "description": "A detailed, buildable model of the human body, helping children understand biology.", + "category": "educational toys", + "brand": "BodyWorks", + "price": 34.5, + "quantity": 60 + }, + { + "name": "Chemistry Lab Set", + "description": "Perform safe and exciting experiments with this comprehensive chemistry set, includes goggles and chemicals.", + "category": "educational toys", + "brand": "Mad Scientist Labs", + "price": 48.25, + "quantity": 50 + }, + { + "name": "Interactive Globe with Pen", + "description": "A talking globe that provides facts about countries, capitals, and cultures when touched with a special pen.", + "category": "educational toys", + "brand": "World Discoverer", + "price": 69.99, + "quantity": 35 + }, + { + "name": "Giant Teddy Bear", + "description": "An extra-large, super soft teddy bear, perfect for big hugs and comforting companionship.", + "category": "plush toys", + "brand": "Cuddle Buddies", + "price": 45.0, + "quantity": 65 + }, + { + "name": "Unicorn Plush Toy", + "description": "A magical plush unicorn with a rainbow mane and sparkling horn, soft and cuddly.", + "category": "plush toys", + "brand": "Fantasy Friends", + "price": 22.5, + "quantity": 100 + }, + { + "name": "Puppy Dog Plush", + "description": "An adorable plush puppy dog with floppy ears and a wagging tail, realistic and huggable.", + "category": "plush toys", + "brand": "Pet Pals", + "price": 18.0, + "quantity": 120 + }, + { + "name": "Dinosaur Stuffed Animal", + "description": "A friendly plush dinosaur, soft and colorful, great for prehistoric adventures.", + "category": "plush toys", + "brand": "Dino Buddies", + "price": 25.75, + "quantity": 90 + }, + { + "name": "Mini Animal Plush Assortment", + "description": "A collection of small, cute plush animals, perfect for party favors or collecting.", + "category": "plush toys", + "brand": "Tiny Treasures", + "price": 9.99, + "quantity": 200 + }, + { + "name": "High-Speed RC Race Car", + "description": "A fast and agile remote control race car with full-function steering and durable design.", + "category": "remote control toys", + "brand": "Speed Racers", + "price": 39.99, + "quantity": 80 + }, + { + "name": "Flying Drone with Camera", + "description": "An easy-to-fly drone equipped with a camera for aerial photography and video recording.", + "category": "remote control toys", + "brand": "SkyView Drones", + "price": 75.0, + "quantity": 40 + }, + { + "name": "RC Monster Truck", + "description": "A rugged remote control monster truck with oversized tires, capable of tackling various terrains.", + "category": "remote control toys", + "brand": "Off-Road RCs", + "price": 55.25, + "quantity": 60 + }, + { + "name": "Remote Control Helicopter", + "description": "A stable and easy-to-control RC helicopter, perfect for indoor flying fun.", + "category": "remote control toys", + "brand": "Heli-Fun", + "price": 29.5, + "quantity": 95 + }, + { + "name": "RC Robot Fighter", + "description": "A remote control robot that can walk, talk, and even battle other robots with soft projectiles.", + "category": "remote control toys", + "brand": "Robot Rumble", + "price": 62.0, + "quantity": 50 + }, + { + "name": "Kids Outdoor Swing Set", + "description": "A sturdy and safe swing set for backyard fun, includes two swings and a glider.", + "category": "outdoor toys", + "brand": "Backyard Adventures", + "price": 199.99, + "quantity": 20 + }, + { + "name": "Water Blaster Super Soaker", + "description": "A powerful water blaster for epic water fights, featuring a large capacity tank.", + "category": "outdoor toys", + "brand": "AquaBlast", + "price": 14.99, + "quantity": 150 + }, + { + "name": "Giant Bubble Wand Kit", + "description": "Create enormous bubbles with this special wand and solution, perfect for outdoor play.", + "category": "outdoor toys", + "brand": "Bubble Magic", + "price": 12.5, + "quantity": 180 + }, + { + "name": "Kids Soccer Goal Set", + "description": "A portable soccer goal set, easy to assemble and perfect for practicing soccer skills in the yard.", + "category": "outdoor toys", + "brand": "Sporty Kids", + "price": 35.0, + "quantity": 70 + }, + { + "name": "Flying Disc Set", + "description": "A set of durable flying discs for throwing and catching games, ideal for parks and beaches.", + "category": "outdoor toys", + "brand": "Disc Fun", + "price": 8.75, + "quantity": 200 + }, + { + "name": "Sand Play Set with Molds", + "description": "A complete sand play set including buckets, shovels, and various molds for creative sandcastles.", + "category": "outdoor toys", + "brand": "Beach Builders", + "price": 16.0, + "quantity": 130 + }, + { + "name": "Jump Rope with Counter", + "description": "A classic jump rope with a built-in counter to track jumps, great for exercise and coordination.", + "category": "outdoor toys", + "brand": "Active Play", + "price": 9.5, + "quantity": 160 + }, + { + "name": "Kite Flying Kit", + "description": "An easy-to-assemble kite with a long string, perfect for windy days at the park.", + "category": "outdoor toys", + "brand": "Wind Riders", + "price": 11.25, + "quantity": 140 + }, + { + "name": "Hula Hoop Assortment", + "description": "Colorful hula hoops in various sizes, promoting active play and coordination.", + "category": "outdoor toys", + "brand": "Hoop Stars", + "price": 7.99, + "quantity": 220 + }, + { + "name": "Outdoor Ring Toss Game", + "description": "A fun and simple ring toss game for all ages, perfect for picnics and backyard parties.", + "category": "outdoor toys", + "brand": "Garden Games", + "price": 19.0, + "quantity": 100 + } + ] +} \ No newline at end of file diff --git a/backend/python/week6/schema.py b/backend/python/week6/schema.py index 6e87621a6..9f20dcdda 100644 --- a/backend/python/week6/schema.py +++ b/backend/python/week6/schema.py @@ -7,7 +7,7 @@ class ProductSchema(BaseModel): description: Annotated[str, Field(min_length=1, max_length=500)] category: Annotated[str, Field(min_length=1, max_length=100)] brand: Annotated[str, Field(min_length=1, max_length=100)] - price: Annotated[float, Field(gt=0)] + price: Annotated[float, Field(ge=0.01)] quantity: Annotated[int, Field(ge=0)] diff --git a/backend/python/week6/synthetic_products.ipynb b/backend/python/week6/synthetic_products.ipynb new file mode 100644 index 000000000..09b3eea8b --- /dev/null +++ b/backend/python/week6/synthetic_products.ipynb @@ -0,0 +1,340 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 11, + "id": "0bbaf6bc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Project root added to sys.path\n", + "C:\\Users\\Kreesh\\OneDrive\\Desktop\\Rippling\\interneers-lab\\backend\\python\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "\n", + "project_root = r\"C:\\Users\\Kreesh\\OneDrive\\Desktop\\Rippling\\interneers-lab\\backend\\python\"\n", + "\n", + "if project_root not in sys.path:\n", + " sys.path.append(project_root)\n", + "\n", + "print(\"Project root added to sys.path\")\n", + "print(sys.path[-1])" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "bf39fe54", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import json\n", + "from decimal import Decimal\n", + "from dotenv import load_dotenv\n", + "from google import genai\n", + "from week4.db_connection import initialize_mongo\n", + "from week4.models import Product, ProductCategory\n", + "from week6.schema import ProductListSchema" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "b0afde86", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Gemini and MongoDB initialized successfully\n" + ] + } + ], + "source": [ + "load_dotenv()\n", + "initialize_mongo()\n", + "\n", + "client = genai.Client(api_key=os.getenv(\"GEMINI_API_KEY\"))\n", + "print(\"Gemini and MongoDB initialized successfully\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c787254a", + "metadata": {}, + "outputs": [], + "source": [ + "prompt = \"\"\"\n", + "Generate exactly 5 products for a toy store.\n", + "\n", + "Return valid JSON only in this structure:\n", + "{\n", + " \"products\": [\n", + " {\n", + " \"name\": \"string\",\n", + " \"description\": \"string\",\n", + " \"category\": \"string\",\n", + " \"brand\": \"string\",\n", + " \"price\": 10.99,\n", + " \"quantity\": 25\n", + " }\n", + " ]\n", + "}\n", + "\n", + "Rules:\n", + "- Include exactly 5 products\n", + "- Product name must never be empty\n", + "- Brand must never be empty\n", + "- Description must never be empty\n", + "- Use realistic toy store categories like puzzles, dolls, action figures, board games, educational toys, building blocks, plush toys, remote control toys, outdoor toys\n", + "- price must be a float greater than 0\n", + "- quantity must be an integer greater than or equal to 0\n", + "- do not include markdown\n", + "- output valid JSON only\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f51fe41c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{\"products\":[{\"name\":\"Starlight Princess Doll\",\"description\":\"A beautiful doll with shimmering gown and magical accessories, perfect for imaginative play.\",\"category\":\"Dolls\",\"brand\":\"Dreamland Toys\",\"price\":29.99,\"quantity\":50},{\"name\":\"Galactic Warrior Action Figure\",\"description\":\"Highly detailed action figure with multiple points of articulation and interchangeable weapons for epic space battles.\",\"category\":\"Action Figures\",\"brand\":\"Cosmic Play\",\"price\":19.50,\"quantity\":75},{\"name\":\"Enchanted Forest Jigsaw Puzzle\",\"description\":\"A 1000-piece jigsaw puzzle featuring a vibrant illustration of a magical forest scene, challenging and fun for all ages.\",\"category\":\"Puzzles\",\"brand\":\"Mind Bender Puzzles\",\"price\":15.75,\"quantity\":30},{\"name\":\"Robo-Racer Remote Control Car\",\"description\":\"High-speed remote control car with durable design and responsive controls, ideal for indoor and outdoor racing.\",\"category\":\"Remote Control Toys\",\"brand\":\"Speedy Wheels\",\"price\":45.00,\"quantity\":20},{\"name\":\"Giant Plush Teddy Bear\",\"description\":\"An extra-large, super soft teddy bear, perfect for cuddling and a wonderful companion for children.\",\"category\":\"Plush Toys\",\"brand\":\"Cuddle Buddies\",\"price\":35.99,\"quantity\":40}]}\n" + ] + } + ], + "source": [ + "response = client.models.generate_content(\n", + " model=\"gemini-2.5-flash\",\n", + " contents=prompt,\n", + " config={\n", + " \"temperature\": 0.3,\n", + " \"response_mime_type\": \"application/json\",\n", + " \"response_schema\": ProductListSchema,\n", + " }\n", + ")\n", + "\n", + "print(response.text[:2000])" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d120b2b2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Validated product count: 5\n" + ] + } + ], + "source": [ + "validated_products = ProductListSchema.model_validate_json(response.text)\n", + "print(\"Validated product count:\", len(validated_products.products))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "7d73e424", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'name': 'Starlight Princess Doll',\n", + " 'description': 'A beautiful doll with shimmering gown and magical accessories, perfect for imaginative play.',\n", + " 'category': 'Dolls',\n", + " 'brand': 'Dreamland Toys',\n", + " 'price': 29.99,\n", + " 'quantity': 50},\n", + " {'name': 'Galactic Warrior Action Figure',\n", + " 'description': 'Highly detailed action figure with multiple points of articulation and interchangeable weapons for epic space battles.',\n", + " 'category': 'Action Figures',\n", + " 'brand': 'Cosmic Play',\n", + " 'price': 19.5,\n", + " 'quantity': 75},\n", + " {'name': 'Enchanted Forest Jigsaw Puzzle',\n", + " 'description': 'A 1000-piece jigsaw puzzle featuring a vibrant illustration of a magical forest scene, challenging and fun for all ages.',\n", + " 'category': 'Puzzles',\n", + " 'brand': 'Mind Bender Puzzles',\n", + " 'price': 15.75,\n", + " 'quantity': 30},\n", + " {'name': 'Robo-Racer Remote Control Car',\n", + " 'description': 'High-speed remote control car with durable design and responsive controls, ideal for indoor and outdoor racing.',\n", + " 'category': 'Remote Control Toys',\n", + " 'brand': 'Speedy Wheels',\n", + " 'price': 45.0,\n", + " 'quantity': 20},\n", + " {'name': 'Giant Plush Teddy Bear',\n", + " 'description': 'An extra-large, super soft teddy bear, perfect for cuddling and a wonderful companion for children.',\n", + " 'category': 'Plush Toys',\n", + " 'brand': 'Cuddle Buddies',\n", + " 'price': 35.99,\n", + " 'quantity': 40}]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product_preview = [product.model_dump() for product in validated_products.products[:5]]\n", + "product_preview" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "4eb0cd0d", + "metadata": {}, + "outputs": [], + "source": [ + "def get_or_create_category(category_title):\n", + " cat_title = category_title.strip()\n", + "\n", + " if not cat_title:\n", + " cat_title = \"Miscellaneous\"\n", + "\n", + " category = ProductCategory.objects(title=cat_title).first()\n", + " if category:\n", + " return category\n", + "\n", + " category = ProductCategory(\n", + " title=cat_title,\n", + " description=f\"Auto-created from Gemini synthetic inventory data for {cat_title}\"\n", + " )\n", + " category.save()\n", + " return category" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "4029f01a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved products: 5\n" + ] + } + ], + "source": [ + "saved_count = 0\n", + "\n", + "for item in validated_products.products:\n", + " new_name = item.name.strip()\n", + " new_brand = item.brand.strip()\n", + " new_description = item.description.strip()\n", + " new_category = item.category.strip()\n", + " new_price = item.price\n", + " new_quantity = item.quantity\n", + " if not new_name:\n", + " print(\"Skipped one product because name was empty after stripping.\")\n", + " continue\n", + "\n", + " if not new_brand:\n", + " print(f\"Skipped product '{new_name}' because brand was empty after stripping.\")\n", + " continue\n", + "\n", + " if not new_description:\n", + " print(f\"Skipped product '{new_name}' because description was empty after stripping.\")\n", + " continue\n", + "\n", + " if new_price is None or new_price <= 0:\n", + " print(f\"Skipped product '{new_name}' because price was invalid: {new_price}\")\n", + " continue\n", + "\n", + " if new_quantity is None or new_quantity < 0:\n", + " print(f\"Skipped product '{new_name}' because quantity was invalid: {new_quantity}\")\n", + " continue\n", + "\n", + " category_obj = get_or_create_category(new_category)\n", + "\n", + " product = Product(\n", + " name=new_name,\n", + " description=new_description,\n", + " price=Decimal(str(item.price)),\n", + " brand=new_brand,\n", + " quantity=int(item.quantity),\n", + " category=category_obj\n", + " )\n", + " product.save()\n", + " saved_count += 1\n", + "\n", + "print(\"Saved products:\", saved_count)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "1fb53d7e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Exported validated products to week6/generated_products.json\n" + ] + } + ], + "source": [ + "import os\n", + "os.makedirs(\"week6\", exist_ok=True)\n", + "\n", + "with open(\"week6/generated_products.json\", \"w\", encoding=\"utf-8\") as file:\n", + " json.dump(\n", + " {\"products\": [product.model_dump() for product in validated_products.products]},\n", + " file,\n", + " indent=2\n", + " )\n", + "\n", + "print(\"Exported validated products to week6/generated_products.json\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.12.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/backend/python/week6/task_1.py b/backend/python/week6/task_1.py index d9a602abd..69b8c1448 100644 --- a/backend/python/week6/task_1.py +++ b/backend/python/week6/task_1.py @@ -37,13 +37,13 @@ def run_prompt(temperature): # metadata : data about data usage = response.usage_metadata # number of tokens in input prompt - prompt_tokens = usage.prompt_token_count + prompt_tokens = getattr(usage, "prompt_token_count", 0) # total tokens used in whole request - total_tokens = usage.total_token_count + total_tokens = getattr(usage, "total_token_count", 0) # number of tokens in visible response text - candidate_tokens = usage.candidates_token_count + candidate_tokens = getattr(usage, "candidates_token_count", 0) # number of tokens used in thinking and internal reasoning - thoughts_tokens = usage.thoughts_token_count + thoughts_tokens = getattr(usage, "thoughts_token_count", 0) print("\nTOKEN USAGE:") print("Prompt tokens:", prompt_tokens) print("Candidate tokens:", candidate_tokens) @@ -60,7 +60,7 @@ def run_prompt(temperature): # temperature controls randomness in token selection # low temperature => less randomness => more stable outcomes -run_prompt(0) +run_prompt(0.0) run_prompt(0.5) run_prompt(1) run_prompt(1.5) diff --git a/backend/python/week6/task_2.py b/backend/python/week6/task_2.py new file mode 100644 index 000000000..b0df13c06 --- /dev/null +++ b/backend/python/week6/task_2.py @@ -0,0 +1,147 @@ +import os +import json +from decimal import Decimal +from dotenv import load_dotenv +from google import genai +from week4.db_connection import initialize_mongo +from week4.models import Product, ProductCategory +from week6.schema import ProductListSchema + +load_dotenv() +initialize_mongo() + +client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) + + +# creating category if it does not exist +def get_create_category(category_title): + cat_title = category_title.strip() + if not cat_title: + cat_title = "Miscellaneous" + category = ProductCategory.objects(title=cat_title).first() + if category: + return category + category = ProductCategory( + title=cat_title, + description=f"Auto-created from Gemini synthetic inventory data for {cat_title}" + ) + category.save() + return category + + +# creating products with gemini +def generate_products_with_gemini(): + prompt = """ +Generate exactly 5 products for a toy store. + +Return valid JSON only in this structure: +{ + "products": [ + { + "name": "string", + "description": "string", + "category": "string", + "brand": "string", + "price": 10.99, + "quantity": 25 + } + ] +} + +Rules: +- Include exactly 5 products +- Product name must never be empty +- Brand must never be empty +- Description must never be empty +- Use realistic toy store categories like puzzles, dolls, action figures, board games, educational toys, building blocks, plush toys, remote control toys, outdoor toys +- price must be a float greater than 0 +- quantity must be an integer greater than or equal to 0 +- do not include markdown +- output valid JSON only +""" + response = client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config={ + "temperature": 0.3, + "response_mime_type": "application/json", + "response_schema": ProductListSchema, + } + ) + print("\nRAW GEMINI RESPONSE:\n") + print(response.text) + validated_data = ProductListSchema.model_validate_json(response.text) + print(f"\nValidated product count: {len(validated_data.products)}") + return validated_data + + +# exporting products as json +def export_product_to_json(validated_data, filepath: str = "week6/generated_products.json"): + product_as_dicts = [] + for product in validated_data.products: + # .model_dump converts each product into dictionary (from pydantic form) + product_dict = product.model_dump() + product_as_dicts.append(product_dict) + with open(filepath, "w", encoding="utf-8") as file: + json.dump({"products": product_as_dicts}, file, indent=2) + print(f"\nValidated products exported to {filepath}") + + +# saving products to week4 db +def save_products_to_week4(validated_data): + saved_count = 0 + + for item in validated_data.products: + new_name = item.name.strip() + new_brand = item.brand.strip() + new_description = item.description.strip() + new_category = item.category.strip() + new_price = item.price + new_quantity = item.quantity + + if not new_name: + print("Skipped one product because name was empty after stripping.") + continue + + if not new_brand: + print( + f"Skipped product '{new_name}' because brand was empty after stripping.") + continue + + if not new_description: + print( + f"Skipped product '{new_name}' because description was empty after stripping.") + continue + + if new_price is None or new_price <= 0: + print( + f"Skipped product '{new_name}' because price was invalid: {new_price}") + continue + + if new_quantity is None or new_quantity < 0: + print( + f"Skipped product '{new_name}' because quantity was invalid: {new_quantity}") + continue + + category_obj = get_create_category(new_category) + + product = Product( + name=new_name, + description=new_description, + price=Decimal(str(new_price)), + brand=new_brand, + quantity=int(new_quantity), + category=category_obj, + ) + product.save() + saved_count += 1 + + return saved_count + + +# run the block only if file is executed directly +if __name__ == "__main__": + validated_data = generate_products_with_gemini() + export_product_to_json(validated_data) + saved = save_products_to_week4(validated_data) + print(f"\nSaved {saved} products into Week 4 MongoDB collection.") diff --git a/backend/python/week6/task_4.py b/backend/python/week6/task_4.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/python/week6/week6/generated_products.json b/backend/python/week6/week6/generated_products.json new file mode 100644 index 000000000..9524a632b --- /dev/null +++ b/backend/python/week6/week6/generated_products.json @@ -0,0 +1,44 @@ +{ + "products": [ + { + "name": "Starlight Princess Doll", + "description": "A beautiful doll with shimmering gown and magical accessories, perfect for imaginative play.", + "category": "Dolls", + "brand": "Dreamland Toys", + "price": 29.99, + "quantity": 50 + }, + { + "name": "Galactic Warrior Action Figure", + "description": "Highly detailed action figure with multiple points of articulation and interchangeable weapons for epic space battles.", + "category": "Action Figures", + "brand": "Cosmic Play", + "price": 19.5, + "quantity": 75 + }, + { + "name": "Enchanted Forest Jigsaw Puzzle", + "description": "A 1000-piece jigsaw puzzle featuring a vibrant illustration of a magical forest scene, challenging and fun for all ages.", + "category": "Puzzles", + "brand": "Mind Bender Puzzles", + "price": 15.75, + "quantity": 30 + }, + { + "name": "Robo-Racer Remote Control Car", + "description": "High-speed remote control car with durable design and responsive controls, ideal for indoor and outdoor racing.", + "category": "Remote Control Toys", + "brand": "Speedy Wheels", + "price": 45.0, + "quantity": 20 + }, + { + "name": "Giant Plush Teddy Bear", + "description": "An extra-large, super soft teddy bear, perfect for cuddling and a wonderful companion for children.", + "category": "Plush Toys", + "brand": "Cuddle Buddies", + "price": 35.99, + "quantity": 40 + } + ] +} \ No newline at end of file From a576ad733195bd2ab95c7569b59757d0dbd5ea12 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 23:02:46 +0530 Subject: [PATCH 62/83] week6 - task 4 completed --- backend/python/week6/future_stock_events.json | 84 +++++++++++++++++++ backend/python/week6/task_4.py | 76 +++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 backend/python/week6/future_stock_events.json diff --git a/backend/python/week6/future_stock_events.json b/backend/python/week6/future_stock_events.json new file mode 100644 index 000000000..189b96553 --- /dev/null +++ b/backend/python/week6/future_stock_events.json @@ -0,0 +1,84 @@ +{ + "events": [ + { + "title": "Action Figures Q4 Restock", + "event_type": "incoming_shipment", + "expected_date": "2023-11-05", + "product_name": "Galaxy Guardians Action Figure Set", + "quantity_change": 300, + "note": "Expected delivery from distributor. High demand item." + }, + { + "title": "Holiday Building Blocks Rush", + "event_type": "seasonal_spike", + "expected_date": "2023-12-10", + "product_name": "MegaBuild Creator Blocks 1000pc", + "quantity_change": 500, + "note": "Anticipating increased sales for holiday season." + }, + { + "title": "Critical Stock: Strategy Board Game", + "event_type": "low_stock_warning", + "expected_date": "2023-11-01", + "product_name": "Quest for the Crystal Throne", + "quantity_change": 100, + "note": "Current stock levels are critically low. Reorder immediately." + }, + { + "title": "New Doll Series Preorder Fulfillment", + "event_type": "preorder_arrival", + "expected_date": "2023-11-15", + "product_name": "Enchanted Friends Collectible Doll", + "quantity_change": 250, + "note": "All preorders for Wave 1 ready for dispatch." + }, + { + "title": "Electronic Gadget Shipment Delay", + "event_type": "supplier_delay", + "expected_date": "2023-11-20", + "product_name": "RoboPet Interactive Companion", + "quantity_change": 0, + "note": "Original delivery date of 2023-11-10 pushed back due to manufacturing issues." + }, + { + "title": "Plush Toy Transfer from Warehouse B", + "event_type": "warehouse_transfer", + "expected_date": "2023-11-07", + "product_name": "CuddleBuddy Plush Bear", + "quantity_change": 100, + "note": "Moving excess stock from off-site warehouse to main store." + }, + { + "title": "Educational Toys New Arrivals", + "event_type": "incoming_shipment", + "expected_date": "2023-11-25", + "product_name": "STEM Explorer Science Kit", + "quantity_change": 180, + "note": "New line of educational toys arriving for holiday season." + }, + { + "title": "Spring Outdoor Play Equipment Demand", + "event_type": "seasonal_spike", + "expected_date": "2024-03-15", + "product_name": "Adventure Playground Swing Set", + "quantity_change": 75, + "note": "Forecasting high demand for outdoor items in early spring." + }, + { + "title": "Low Stock: Premium Art Supplies", + "event_type": "low_stock_warning", + "expected_date": "2023-11-03", + "product_name": "Artist's Deluxe Paint Set", + "quantity_change": 50, + "note": "Running low on popular art sets. Need to reorder." + }, + { + "title": "Limited Edition Model Car Preorder", + "event_type": "preorder_arrival", + "expected_date": "2023-12-01", + "product_name": "Vintage Racer Die-Cast Model", + "quantity_change": 50, + "note": "Exclusive release, all preorders allocated." + } + ] +} \ No newline at end of file diff --git a/backend/python/week6/task_4.py b/backend/python/week6/task_4.py index e69de29bb..4180a862b 100644 --- a/backend/python/week6/task_4.py +++ b/backend/python/week6/task_4.py @@ -0,0 +1,76 @@ +import os +import json +from dotenv import load_dotenv +from google import genai +from week6.schema import FutureStockEventListSchema + +load_dotenv() + +client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) + + +def generate_future_stock_events(): + prompt = """ +Generate exactly 10 future stock events for a toy store. + +Return valid JSON only in this structure: +{ + "events": [ + { + "title": "string", + "event_type": "string", + "expected_date": "YYYY-MM-DD", + "product_name": "string", + "quantity_change": 10, + "note": "string" + } + ] +} + +Rules: +- Include exactly 10 events +- All dates must be future dates +- Keep events realistic for toy inventory +- event_type can be: + incoming_shipment, + seasonal_spike, + low_stock_warning, + preorder_arrival, + supplier_delay, + warehouse_transfer +- title must not be empty +- product_name must not be empty +- quantity_change must be an integer +- output valid JSON only +- do not include markdown +""" + response = client.models.generate_content( + model="gemini-2.5-flash", + contents=prompt, + config={ + "temperature": 0.3, + "response_mime_type": "application/json", + "response_schema": FutureStockEventListSchema, + } + ) + print("\nRAW GEMINI RESPONSE:\n") + print(response.text) + validated_events = FutureStockEventListSchema.model_validate_json( + response.text) + print(f"\nValidated event count: {len(validated_events.events)}") + return validated_events + + +def export_events_to_json(validated_events, filepath: str = "week6/future_stock_events.json"): + events_as_dicts = [] + for event in validated_events.events: + event_dict = event.model_dump() + events_as_dicts.append(event_dict) + with open(filepath, "w", encoding="utf-8") as file: + json.dump({"events": events_as_dicts}, file, indent=2) + print(f"\nValidated events exported to {filepath}") + + +if __name__ == "__main__": + validated_events = generate_future_stock_events() + export_events_to_json(validated_events) From 5e9a9133e26ca78f51795a23e01652b387f31e68 Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Tue, 31 Mar 2026 23:51:37 +0530 Subject: [PATCH 63/83] readme updated --- backend/python/README.md | 38 +++ backend/python/week6/dashboard.py | 399 ++++++++++++++++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 backend/python/week6/dashboard.py diff --git a/backend/python/README.md b/backend/python/README.md index 5d4168deb..327a3be44 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -471,6 +471,44 @@ This runs automatically — no manual step needed. The process is idempotent: al --- +# Week 6 - Structured LLM Output — Validation & JSON with Pydantic + +LLMs return plain text. Even when asked for JSON, they can produce malformed output, include markdown fences, omit fields, use wrong types, or return the wrong number of items. Without validation, this silently breaks your application. + +--- + +## Pydantic Schemas + +Pydantic schemas define the exact shape and constraints data must meet — field types, min/max lengths, numeric ranges, and optional vs required fields. If the LLM response doesn't match, a `ValidationError` is raised immediately and nothing bad reaches the database. + +--- + +## Two Layers of Enforcement + +**Prompt level** — the prompt shows the exact JSON structure, uses concrete numeric examples, forbids markdown, and states field rules explicitly. + +**API level** — Gemini's `response_mime_type` and `response_schema` config options constrain the model's output at generation time, before it even reaches your code. + +Both layers together are more reliable than either one alone. + +--- + +## Parse → Validate → Save + +1. `model_validate_json()` parses the raw response text and validates all fields in one step. +2. Defensive checks (`.strip()`, range checks) catch edge cases that Pydantic technically allows but would produce bad data in the database. +3. `model_dump()` converts validated Pydantic objects back to plain Python dicts for JSON export or database saving. + +--- + +## Flow + +``` +Prompt → Gemini API → Raw JSON → model_validate_json() → Defensive checks → MongoDB / JSON file +``` + +--- + BN4EJx^)+cJj-B=S7yqtSMz#dWLjxx- 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 literal 0 HcmV?d00001 diff --git a/backend/python/week9_10/task3_final_output.png b/backend/python/week9_10/task3_final_output.png new file mode 100644 index 0000000000000000000000000000000000000000..6ec32ba18b005453a208c17f0451630b08e28516 GIT binary patch literal 138857 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 Cp2k1` literal 0 HcmV?d00001 diff --git a/backend/python/week9_10/tools.py b/backend/python/week9_10/tools.py index ca160d469..334e0b1d5 100644 --- a/backend/python/week9_10/tools.py +++ b/backend/python/week9_10/tools.py @@ -34,7 +34,7 @@ def product_to_dict(product): # LLM sees the user message + tool descriptions # LLM outputs: "please call get_product_info with product_id=1" — this is a request, not a result # LangChain reads that request and dispatches: get_product_info.invoke({"product_id": 1}) -# LangChain runs the function body — EXACTLY THE SAME AS CASE 1 +# LangChain runs the function body — same as case 1 # product_repository.get(1) actually hits MongoDB # Function returns the dict to LangChain # LangChain takes the dict and sends it to the LLM in the next API call as a "tool result" diff --git a/backend/python/week9_10/tools_testing.ipynb b/backend/python/week9_10/tools_testing.ipynb index c9940ff20..01650ff7c 100644 --- a/backend/python/week9_10/tools_testing.ipynb +++ b/backend/python/week9_10/tools_testing.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 11, + "execution_count": 1, "id": "39822498", "metadata": {}, "outputs": [ @@ -30,10 +30,19 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 2, "id": "6ea791da", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "c:\\Users\\Kreesh\\OneDrive\\Desktop\\Rippling\\interneers-lab\\backend\\python\\venv\\Lib\\site-packages\\tqdm\\auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n", + " from .autonotebook import tqdm as notebook_tqdm\n" + ] + } + ], "source": [ "from week9_10.tools import find_product_by_query, get_product_info, check_inventory, calculate_quote" ] @@ -50,7 +59,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 3, "id": "08d5cab7", "metadata": {}, "outputs": [ @@ -104,7 +113,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "id": "97bf2e05", "metadata": {}, "outputs": [ @@ -114,7 +123,28 @@ "text": [ "============================================================\n", "SECTION 2: find_product_by_query\n", - "============================================================\n", + "============================================================\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.\n", + "Loading weights: 100%|██████████| 103/103 [00:00<00:00, 5040.94it/s]\n", + "\u001b[1mBertModel LOAD REPORT\u001b[0m from: sentence-transformers/all-MiniLM-L6-v2\n", + "Key | Status | | \n", + "------------------------+------------+--+-\n", + "embeddings.position_ids | UNEXPECTED | | \n", + "\n", + "Notes:\n", + "- UNEXPECTED:\tcan be ignored when loading from different task/architecture; not ok if you expect identical arch.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ "Query: 'building blocks'\n", " id= 10 name=Deluxe Creative Building Blocks Set score=0.6179\n", " id= 15 name=Deluxe City Building Blocks Set score=0.5998\n", @@ -156,7 +186,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 5, "id": "44ddc6e7", "metadata": {}, "outputs": [ @@ -200,7 +230,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 6, "id": "2d4e26fd", "metadata": {}, "outputs": [ @@ -244,7 +274,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 7, "id": "4ab6037e", "metadata": {}, "outputs": [ @@ -292,7 +322,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 8, "id": "1f446a0c", "metadata": {}, "outputs": [ @@ -340,7 +370,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 9, "id": "1ac9f487", "metadata": {}, "outputs": [ @@ -387,7 +417,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 10, "id": "51e0eede", "metadata": {}, "outputs": [ From 75e67caaae1403d09dba6f3ec08b59ce8ccc225a Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Thu, 14 May 2026 13:50:02 +0530 Subject: [PATCH 82/83] week9_10 - eval and langsmith added --- backend/python/week9_10/agent.py | 2 +- backend/python/week9_10/eval_agent.py | 208 +++++++++++++++++++++ backend/python/week9_10/langsimth_setup.py | 37 ++++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 backend/python/week9_10/eval_agent.py create mode 100644 backend/python/week9_10/langsimth_setup.py diff --git a/backend/python/week9_10/agent.py b/backend/python/week9_10/agent.py index f22b6b732..56b75ec6b 100644 --- a/backend/python/week9_10/agent.py +++ b/backend/python/week9_10/agent.py @@ -3,7 +3,7 @@ from langchain_classic.agents import AgentExecutor, create_tool_calling_agent from langchain_core.prompts import ChatPromptTemplate -from week8.langsmith_setup import setup_langsmith_tracing +from week9_10.langsimth_setup import setup_langsmith_tracing from week9_10.config import AGENT_MAX_ITERATIONS from week9_10.llm_client import get_chat_model from week9_10.tools import ( diff --git a/backend/python/week9_10/eval_agent.py b/backend/python/week9_10/eval_agent.py new file mode 100644 index 000000000..585146109 --- /dev/null +++ b/backend/python/week9_10/eval_agent.py @@ -0,0 +1,208 @@ +from week9_10.service import run_quote_service +from week9_10.policy import is_within_policy + +EVAL_TEST_CASES = [ + # SIMPLE SCENARIOS - clean inputs, expected smooth flow + { + "name": "simple_tier1_no_discount", + "query": "I want a quote for 5 building blocks", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": 0.00, + "expected_quantity": 5, + "expected_keywords": ["building blocks"], + "should_have_invoice": True, + "should_have_stock_warning": False, + }, + { + "name": "simple_tier2_five_percent", + "query": "Can I get pricing for 25 building blocks?", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": 0.05, + "expected_quantity": 25, + "expected_keywords": ["building blocks", "5%"], + "should_have_invoice": True, + "should_have_stock_warning": False, + }, + { + "name": "simple_tier3_ten_percent_spec_example", + "query": "I need 60 building blocks for a school project", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": 0.10, + "expected_quantity": None, # depends on stock, allow either 60 or capped + "expected_keywords": ["building blocks", "10%"], + "should_have_invoice": True, + "should_have_stock_warning": None, # unknown without knowing stock + }, + { + "name": "simple_tier4_fifteen_percent", + "query": "Quote for 120 building blocks please", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": 0.15, + "expected_quantity": None, # may be capped by stock + "expected_keywords": ["building blocks", "15%"], + "should_have_invoice": True, + "should_have_stock_warning": None, + }, + { + "name": "simple_tier2_alternate_phrasing", + "query": "How much would 30 lego cost?", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": 0.05, + "expected_quantity": 30, + "expected_keywords": ["5%"], + "should_have_invoice": True, + "should_have_stock_warning": False, + }, + + # COMPLEX SCENARIOS - edge cases that stress the agent + { + "name": "complex_quantity_above_stock", + "query": "I need 9999 building blocks", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": None, # tier depends on the capped quantity + "expected_quantity": None, # whatever stock currently is + "expected_keywords": ["stock"], # answer should mention stock issue + "should_have_invoice": True, + "should_have_stock_warning": True, # this is the key check + }, + { + "name": "complex_boundary_exactly_50", + "query": "I want a quote for exactly 50 building blocks", + "expected_tools": ["find_product_by_query", "check_inventory", "calculate_quote"], + "expected_discount": 0.10, # 50 falls in 50-99 tier + "expected_quantity": None, # may equal stock if stock=50 + "expected_keywords": ["10%"], + "should_have_invoice": True, + "should_have_stock_warning": None, + }, + { + "name": "complex_non_existent_product", + "query": "I need 20 spaceships from Mars please", + # at minimum, search must run + "expected_tools": ["find_product_by_query"], + "expected_discount": None, + "expected_quantity": None, + "expected_keywords": [], # we don't enforce wording on graceful failures + "should_have_invoice": False, # no real product = no invoice + "should_have_stock_warning": None, + }, +] + + +def evaluate_case(case): + query = case["query"] + result = run_quote_service(query) + invoice = result["invoice"] + answer = result["answer"] + tools_called = [step["tool"] for step in result["intermediate_steps"]] + checks = {} + + # check 1 : were the expected tools called ? + expected_tools = set(case["expected_tools"]) + tools_called_set = set(tools_called) + checks["expected_tools_called"] = expected_tools.issubset(tools_called_set) + + # check 2 : is invoive matching expectation ? + has_invoice = invoice is not None + checks["invoice_presence"] = has_invoice == case["should_have_invoice"] + + # check 3 : discount rates + if invoice and case["expected_discount"] is not None: + checks["discount_rate"] = invoice["discount_rate"] == case["expected_discount"] + else: + checks["discount_rate"] = True + + # check 4 : quoted quantity + if invoice and case["expected_quantity"] is not None: + checks["quoted_quantity"] = invoice["quantity"] == case["expected_quantity"] + else: + checks["quoted_quantity"] = True + + # check 5 : expected keywork appears in answer text + answer_lower = answer.lower() + missing_keywords = [kw for kw in case["expected_keywords"] + if kw.lower() not in answer_lower] + checks["expected_keywords"] = len(missing_keywords) == 0 + + # check 6 : stock warning + if invoice and case["should_have_stock_warning"] is not None: + has_stock_warning = invoice.get("stock_warning") is not None + checks["stock_warning"] = has_stock_warning == case["should_have_stock_warning"] + else: + checks["stock_warning"] = True + + # check 7 : policy should never be violated + if invoice: + checks["policy_within_bounds"] = is_within_policy( + invoice["discount_rate"]) + else: + checks["policy_within_bounds"] = True # skipped + + overall_passed = all(checks.values()) + + return { + "name": case["name"], + "query": query, + "checks": checks, + "missing_keywords": missing_keywords if not checks["expected_keywords"] else [], + "tools_called": tools_called, + "discount_rate": invoice["discount_rate"] if invoice else None, + "quoted_quantity": invoice["quantity"] if invoice else None, + "stock_warning": invoice.get("stock_warning") if invoice else None, + "overall_passed": overall_passed, + } + + +def run_eval_suite(): + print("=" * 80) + print("AI QUOTE AGENT - EVAL SUITE") + print("=" * 80) + + results = [] + for case in EVAL_TEST_CASES: + print(f"\nRunning: {case['name']}") + print(f" Query: {case['query']}") + try: + result = evaluate_case(case) + except Exception as e: + print(f" ERROR during evaluation: {e}") + result = { + "name": case["name"], + "query": case["query"], + "checks": {}, + "overall_passed": False, + "error": str(e), + } + results.append(result) + + status = "PASS" if result["overall_passed"] else "FAIL" + print(f" Status: {status}") + if not result["overall_passed"]: + failed_checks = [k for k, v in result["checks"].items() if not v] + print(f" Failed checks: {failed_checks}") + if result.get("missing_keywords"): + print(f" Missing keywords: {result['missing_keywords']}") + print(f" Tools called: {result.get('tools_called')}") + print(f" Discount rate: {result.get('discount_rate')}") + print(f" Quoted quantity: {result.get('quoted_quantity')}") + print(f" Stock warning: {result.get('stock_warning')}") + + # summary + total = len(results) + passed = sum(1 for r in results if r["overall_passed"]) + score = passed / total if total else 0 + + print() + print("=" * 80) + print("SUMMARY") + print("=" * 80) + print(f"Total cases: {total}") + print(f"Passed: {passed}") + print(f"Failed: {total - passed}") + print(f"Score: {score:.2%}") + + return results + + +if __name__ == "__main__": + run_eval_suite() diff --git a/backend/python/week9_10/langsimth_setup.py b/backend/python/week9_10/langsimth_setup.py new file mode 100644 index 000000000..bb6caad8c --- /dev/null +++ b/backend/python/week9_10/langsimth_setup.py @@ -0,0 +1,37 @@ +# enables langsmith tracing for the week 9 / 10 quote agent +# pulls the project name from week 9_10 config so traces land in a separate + +import os +from dotenv import load_dotenv +from week9_10.config import TRACING_PROJECT_NAME + + +load_dotenv() + + +def setup_langsmith_tracing(): + api_key = os.getenv("LANGSMITH_API_KEY") + if not api_key: + return False + os.environ["LANGSMITH_API_KEY"] = api_key + os.environ["LANGSMITH_TRACING"] = "true" + os.environ["LANGSMITH_PROJECT"] = TRACING_PROJECT_NAME + endpoint = os.getenv("LANGSMITH_ENDPOINT") + if endpoint: + os.environ["LANGSMITH_ENDPOINT"] = endpoint + return True + + +def is_langsmith_enabled(): + return os.getenv("LANGSMITH_TRACING", "").lower() == "true" + + +if __name__ == "__main__": + enabled = setup_langsmith_tracing() + if enabled: + print("LangSmith tracing is enabled.") + print(f"Project: {os.getenv('LANGSMITH_PROJECT')}") + if os.getenv("LANGSMITH_ENDPOINT"): + print(f"Endpoint: {os.getenv('LANGSMITH_ENDPOINT')}") + else: + print("LangSmith tracing is disabled because LANGSMITH_API_KEY was not found.") From 37fe4fb74bc9294ca6d647645519da780b37e4ec Mon Sep 17 00:00:00 2001 From: kreeeesh17 Date: Fri, 15 May 2026 17:49:53 +0530 Subject: [PATCH 83/83] week9_10 - readme updated --- backend/python/README.md | 35 ++++++++++++++++++ backend/python/week4/bulk_csv_upload.txt | 4 ++ .../data_level0.bin | Bin 167600 -> 167600 bytes .../length.bin | Bin 400 -> 400 bytes 4 files changed, 39 insertions(+) create mode 100644 backend/python/week4/bulk_csv_upload.txt diff --git a/backend/python/README.md b/backend/python/README.md index f7c7b1e18..cafc0367c 100644 --- a/backend/python/README.md +++ b/backend/python/README.md @@ -647,6 +647,41 @@ week8/ --- +# Week 9/10 — AI Quote Agent + +An autonomous LLM agent built on top of the existing inventory project. Customers describe what they want in natural language, and the agent identifies the product, checks stock, applies tiered discounts within a strict policy cap, and returns a structured Quote Invoice. + +## How It Works + +1. Receives a natural language quote request (e.g. _"I need 60 building blocks for a school project"_) +2. Uses **LangChain** to orchestrate a tool-calling agent backed by **Gemini** +3. The agent identifies the product via **semantic search** (reused from Week 7) +4. Checks current stock level from **MongoDB** +5. Caps the quoted quantity at available stock if the request exceeds it +6. Applies a **tiered discount** (0% / 5% / 10% / 15%) based on quantity +7. Enforces a hard **20% policy cap** on any discount (deterministic Python guard) +8. Produces both a customer-facing answer and a validated **JSON Quote Invoice** (Pydantic) +9. Supports **LangSmith tracing** for full observability of every agent run +10. Includes an automated eval suite covering simple and complex scenarios + +## Project Structure + +``` +week9_10/ +├── config.py # Central settings (model, discount tiers, policy cap) +├── schema.py # QuoteInvoiceSchema - Pydantic model + validation +├── policy.py # Discount cap enforcement +├── llm_client.py # ChatGoogleGenerativeAI wrapper +├── tools.py # The four @tool functions the agent can call +├── agent.py # LangChain AgentExecutor + system prompt +├── service.py # Cleanup layer + structured invoice builder +├── eval_agent.py # Rate-limit-aware test suite +├── tool_tests.ipynb # Jupyter notebook testing each tool in isolation +└── langsmith_setup.py # Enables LangSmith tracing +``` + +--- +