Skip to content

Commit

Permalink
Initial 🎺
Browse files Browse the repository at this point in the history
  • Loading branch information
Sibyx committed Sep 7, 2024
0 parents commit dc445be
Show file tree
Hide file tree
Showing 26 changed files with 1,273 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.idea/
.github/
.git/
.venv/
.stoplight/
__pycache__
*.DS_Store
*.pyo
venv/
evil_flowers_catalog.egg-info/
.editorconfig
.env
.env.example
.flake8
.gitignore
data/
logs/
media/
docs/
gen/
static/
compose.local.yml
26 changes: 26 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
root = true

# Unix-style newlines with a newline ending every file
[*]
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8

# https://www.python.org/dev/peps/pep-0008/
[*.py]
indent_style = space
indent_size = 4
max_line_length = 119

[*.html]
indent_style = tab
indent_size = 2

[*.md]
max_line_length = 119
indent_size = 4

[*.json]
indent_style = space
indent_size = 2
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NOTION_API_TOKEN=
66 changes: 66 additions & 0 deletions .github/workflows/github-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Docker build

on:
push:
branches: [ master ]
# Publish semver tags as releases.
tags: [ '*.*.*' ]
pull_request:
branches: [ master ]

env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
DOCKER_BUILDKIT: 1

jobs:
build:

runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
packages: write

steps:
- name: Install poetry
run: |
sudo apt install -y pipx
pipx ensurepath
pipx install poetry
pipx inject poetry poetry-plugin-export
- name: Checkout repository
uses: actions/checkout@v4

- name: Create requirements.txt
run: |
poetry export -f requirements.txt --without-hashes --output requirements.txt -E docker
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
id: push
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
15 changes: 15 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: Tests

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
- uses: psf/black@stable
with:
options: ". --check"
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.idea/
.venv/
__pycache__
*.DS_Store
*.pyc
*.pyo
*.log
.env
requirements.txt
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

This changelog suppose to follow rules defined in the [changelog.md](https://changelog.md)

## 0.1.0 - 2024-09-07

Initial ready-to-production release (don't take this statement too seriously - I used it for personal inventory).

- **Added**: Label generator for Brother P-Touch editor (`*.lbx` files)
- **Added**: QR code generator
- **Added**: `notion://` redirect for Notion Page ID
38 changes: 38 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
FROM python:3.12-slim AS builder

# System setup
RUN apt update -y && apt install -y libjpeg-dev

WORKDIR /usr/src/app

# Copy source
COPY requirements.txt requirements.txt

## Python environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

# Dependencies
RUN pip install --user -r requirements.txt --no-cache-dir

FROM python:3.12-slim

# Dependencies
RUN apt update -y && apt install -y supervisor curl

WORKDIR /usr/src/app

COPY . .
COPY --from=builder /root/.local /root/.local

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PATH=/root/.local/bin:$PATH

# Configuration
COPY conf/supervisor.conf /etc/supervisord.conf
# Health check
HEALTHCHECK CMD curl --fail http://localhost:8000/v1/status || exit 1

# Execution
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Griminventory API

## Overview

**Griminventory API** is a Flask-based service designed to generate QR code labels for items stored in Notion.
The project includes support for Brother P-touch Editor by generating `.lbx` files. Each label contains the Notion
item title and a QR code linking to the item's page.

## Features

- Redirect to Notion page based on item UUID.
- Generate QR codes for Notion items.
- Generate `.lbx` files for Brother P-touch Editor (62mm landscape format).
- Labels include item titles and QR codes linking to the Notion page.

## Installation

## Usage Endpoints

| Endpoint | Method | Description |
|----------------------------|--------|-------------------------------------------------------------------------------------------|
| `/v1/items/:item_uuid` | GET | Redirects to the corresponding Notion page using the provided `item_uuid`. |
| `/v1/items/:item_uuid.png` | GET | Returns a QR code PNG linking to the Notion page for the given `item_uuid`. |
| `/v1/items/:item_uuid.lbx` | GET | Returns a `.lbx` file for Brother P-touch Editor containing the Notion title and QR code. |

9 changes: 9 additions & 0 deletions conf/supervisor.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[supervisord]
nodaemon=true

[program:gunicorn]
directory=/usr/src/app
command=/root/.local/bin/gunicorn -b 0.0.0.0:8000 -w 4 griminventory_api.__main__:app --log-level=debug --log-file=/var/log/gunicorn.log --timeout 240
autostart=true
autorestart=true
priority=900
11 changes: 11 additions & 0 deletions griminventory_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from flask import Flask

from griminventory_api.api.items import items
from griminventory_api.api.status import status


def create_app() -> Flask:
app = Flask(__name__)
app.register_blueprint(items)
app.register_blueprint(status)
return app
6 changes: 6 additions & 0 deletions griminventory_api/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import create_app

app = create_app()

if __name__ == "__main__":
app.run()
1 change: 1 addition & 0 deletions griminventory_api/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from flask import Blueprint
49 changes: 49 additions & 0 deletions griminventory_api/api/items.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from flask import request, send_file, redirect, Response, Blueprint

from griminventory_api.notion.service import get_notion_page_data
from griminventory_api.utils.lbx_generator import create_lbx_file
from griminventory_api.utils.qr_code_generator import generate_qr_code_image
from io import BytesIO

items = Blueprint("items", __name__)


@items.route("/v1/items/<item_uuid>.png", methods=["GET"])
def generate_qr_code(item_uuid: str) -> Response:
qr_link = request.url_root.strip("/") + f"/v1/items/{item_uuid}"
qr_img = generate_qr_code_image(qr_link)

img_io = BytesIO()
qr_img.save(img_io, "PNG")
img_io.seek(0)

return send_file(img_io, mimetype="image/png")


@items.route("/v1/items/<item_uuid>.lbx", methods=["GET"])
def generate_lbx_file(item_uuid: str) -> Response:
"""Generates and returns a Brother .lbx file for the Notion item."""
# Fetch page data from Notion
notion_data = get_notion_page_data(item_uuid)
title = notion_data.get("properties", {}).get("Name", {}).get("title", [{}])[0].get("plain_text", "Unknown Item")
qr_link = request.url_root.strip("/") + f"/v1/items/{item_uuid}"

# Generate the binary .lbx content
lbx_content = create_lbx_file(qr_data=qr_link, text_data=title)

# Return the LBX file as binary content
lbx_io = BytesIO()
lbx_io.write(lbx_content)
lbx_io.seek(0)

return send_file(lbx_io, mimetype="application/zip", as_attachment=True, download_name=f"{title}.lbx")


@items.route("/v1/items/<item_uuid>", methods=["GET"])
def redirect_to_notion(item_uuid: str) -> Response:
notion_data = get_notion_page_data(item_uuid)
notion_url = notion_data.get("url").replace("https://", "notion://")
if notion_url:
return redirect(notion_url)
else:
return Response("Notion page not found", status=404)
10 changes: 10 additions & 0 deletions griminventory_api/api/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import datetime

from flask import Response, jsonify, Blueprint

status = Blueprint("status", __name__)


@status.route("/v1/status", methods=["GET"])
def status_endpoint() -> Response:
return jsonify({"timestamp": datetime.datetime.isoformat(datetime.datetime.now(datetime.UTC))})
3 changes: 3 additions & 0 deletions griminventory_api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import os

NOTION_API_TOKEN = os.getenv("NOTION_API_TOKEN", "your_notion_api_token")
Empty file.
14 changes: 14 additions & 0 deletions griminventory_api/notion/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import requests
from typing import Dict, Any
from griminventory_api.config import NOTION_API_TOKEN

NOTION_API_URL = "https://api.notion.com"


def get_notion_page_data(item_uuid: str) -> Dict[str, Any]:
headers = {"Authorization": f"Bearer {NOTION_API_TOKEN}", "Notion-Version": "2022-06-28"}
response = requests.get(f"{NOTION_API_URL}/v1/pages/{item_uuid}", headers=headers)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"Failed to fetch Notion page data: {response.status_code}")
Loading

0 comments on commit dc445be

Please sign in to comment.