Compare commits
2 Commits
ddc10c25a0
...
721fbeed66
Author | SHA1 | Date | |
---|---|---|---|
|
721fbeed66 | ||
|
d39e8d727b |
30
.drone.yml
Normal file
30
.drone.yml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
steps:
|
||||||
|
- name: container-build
|
||||||
|
image: 'docker:dind'
|
||||||
|
environment:
|
||||||
|
USERNAME:
|
||||||
|
from_secret: docker_username
|
||||||
|
PASSWORD:
|
||||||
|
from_secret: docker_password
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- >-
|
||||||
|
if [ "${DRONE_BRANCH}" = "main" ]; then export
|
||||||
|
BUILD_TAG="latest"; else export BUILD_TAG="${DRONE_BRANCH}"; fi;
|
||||||
|
- docker login -u $USERNAME -p $PASSWORD git.argideli.com
|
||||||
|
- >-
|
||||||
|
docker build -t
|
||||||
|
git.argideli.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}:$BUILD_TAG
|
||||||
|
.
|
||||||
|
- >-
|
||||||
|
docker push
|
||||||
|
git.argideli.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}:$BUILD_TAG
|
||||||
|
volumes:
|
||||||
|
- name: dockersock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
160
.gitignore
vendored
Normal file
160
.gitignore
vendored
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Use the official Python base image
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
# Copy the requirements file to the working directory
|
||||||
|
COPY ./requirements.txt /code/requirements.txt
|
||||||
|
|
||||||
|
# Install the Python dependencies
|
||||||
|
RUN pip config --user set global.progress_bar off
|
||||||
|
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||||
|
|
||||||
|
# Copy the application code to the working directory
|
||||||
|
COPY ./app /code/app
|
||||||
|
COPY ./app/dot_env /code/app/.env
|
||||||
|
COPY ./app/favicon.ico /code/favicon.ico
|
||||||
|
COPY ./app/favicon.ico /code/app/favicon.ico
|
||||||
|
|
||||||
|
# Expose the port on which the application will run
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Run the FastAPI application using uvicorn server
|
||||||
|
#CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
|
||||||
|
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
|
45
README.md
45
README.md
@ -1,2 +1,45 @@
|
|||||||
# qr-api
|
# Python QR Codes API
|
||||||
|
|
||||||
|
Python API for adding QR Codes in invoices
|
||||||
|
|
||||||
|
# Deployment
|
||||||
|
|
||||||
|
docker-compose up
|
||||||
|
|
||||||
|
The signed invoices are saved at the invoices folder, their filenames are inv_code.pdf
|
||||||
|
|
||||||
|
# Example Calls
|
||||||
|
## Sign with QR
|
||||||
|
<ins>HTTP-POST</ins>
|
||||||
|
|
||||||
|
https://qrcode.argideli.com/signinvoice/?company=Quertex&inv_type=tpy&in_all=0&preset=inv2&inv_series=2&inv_num=5578
|
||||||
|
|
||||||
|
The call body is a multipart form-data, containing the actual invoice. Upon signing with the QR code the new file is returned
|
||||||
|
|
||||||
|
<ins>Parameters</ins>
|
||||||
|
|
||||||
|
* file: UploadFile = File(...) = File to be signed
|
||||||
|
* preset: str = Invoice Preset Selection
|
||||||
|
* code_content: str = QR code Data, if empty the code data that will be put inside the QR will be the url to download the invoice
|
||||||
|
* company: str = Invoice type, english letters (ex. Quertex), NOTE: The Upper Case letter WILL be carried to the filename
|
||||||
|
* inv_type: str = Invoice type, english letters (ex. tpy)
|
||||||
|
* inv_series: str = Invoice series, english letters (ex. 2 or B1)
|
||||||
|
* inv_num: str = Invoice Number
|
||||||
|
* in_all: bool = Sign all pages (0|1)
|
||||||
|
|
||||||
|
|
||||||
|
## Get invoice
|
||||||
|
<ins>HTTP-GET</ins>
|
||||||
|
|
||||||
|
https://qrcode.argideli.com/getinvoice/?inv_code=Quertex_tpy_2_5578
|
||||||
|
|
||||||
|
Get a saved invoice by calling an HTTP/GET and providing the filename as the inv_code parameter, alternatevily the identifier can be reconstructed from the company, inv_type, inv_series, inv_num parameters
|
||||||
|
|
||||||
|
<ins>Parameters</ins>
|
||||||
|
|
||||||
|
* inv_code: str = Invoice identifier/Filename without .pdf extention
|
||||||
|
* company: str = Invoice type, english letters (ex. Quertex), NOTE: The Upper Case letter WILL be carried to the filename
|
||||||
|
* inv_type: str = Invoice type, english letters (ex. tpy)
|
||||||
|
* inv_series: str = Invoice series, english letters (ex. 2 or B1)
|
||||||
|
* inv_num: str = Invoice Number
|
||||||
|
* search_file: bool = Get file by reconstructing the filename from (company, inv_type, inv_series, inv_num), (0|1)
|
||||||
|
160
app/.gitignore
vendored
Normal file
160
app/.gitignore
vendored
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
7
app/dot_env
Normal file
7
app/dot_env
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
SERVICE_URL= "https://qrcode.argideli.com"
|
||||||
|
DESTINATION = "/var/invoices/"
|
||||||
|
|
||||||
|
#SERVICE_URL= "http://127.0.0.1:8000"
|
||||||
|
#DESTINATION = "../invoices/"
|
||||||
|
|
||||||
|
CHUNK_SIZE =1048576 # = 2 ** 20 = 1MB
|
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
24
app/favicon.svg
Normal file
24
app/favicon.svg
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
width="800px" height="800px" viewBox="0 0 45.973 45.973"
|
||||||
|
xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path d="M43.454,18.443h-2.437c-0.453-1.766-1.16-3.42-2.082-4.933l1.752-1.756c0.473-0.473,0.733-1.104,0.733-1.774
|
||||||
|
c0-0.669-0.262-1.301-0.733-1.773l-2.92-2.917c-0.947-0.948-2.602-0.947-3.545-0.001l-1.826,1.815
|
||||||
|
C30.9,6.232,29.296,5.56,27.529,5.128V2.52c0-1.383-1.105-2.52-2.488-2.52h-4.128c-1.383,0-2.471,1.137-2.471,2.52v2.607
|
||||||
|
c-1.766,0.431-3.38,1.104-4.878,1.977l-1.825-1.815c-0.946-0.948-2.602-0.947-3.551-0.001L5.27,8.205
|
||||||
|
C4.802,8.672,4.535,9.318,4.535,9.978c0,0.669,0.259,1.299,0.733,1.772l1.752,1.76c-0.921,1.513-1.629,3.167-2.081,4.933H2.501
|
||||||
|
C1.117,18.443,0,19.555,0,20.935v4.125c0,1.384,1.117,2.471,2.501,2.471h2.438c0.452,1.766,1.159,3.43,2.079,4.943l-1.752,1.763
|
||||||
|
c-0.474,0.473-0.734,1.106-0.734,1.776s0.261,1.303,0.734,1.776l2.92,2.919c0.474,0.473,1.103,0.733,1.772,0.733
|
||||||
|
s1.299-0.261,1.773-0.733l1.833-1.816c1.498,0.873,3.112,1.545,4.878,1.978v2.604c0,1.383,1.088,2.498,2.471,2.498h4.128
|
||||||
|
c1.383,0,2.488-1.115,2.488-2.498v-2.605c1.767-0.432,3.371-1.104,4.869-1.977l1.817,1.812c0.474,0.475,1.104,0.735,1.775,0.735
|
||||||
|
c0.67,0,1.301-0.261,1.774-0.733l2.92-2.917c0.473-0.472,0.732-1.103,0.734-1.772c0-0.67-0.262-1.299-0.734-1.773l-1.75-1.77
|
||||||
|
c0.92-1.514,1.627-3.179,2.08-4.943h2.438c1.383,0,2.52-1.087,2.52-2.471v-4.125C45.973,19.555,44.837,18.443,43.454,18.443z
|
||||||
|
M22.976,30.85c-4.378,0-7.928-3.517-7.928-7.852c0-4.338,3.55-7.85,7.928-7.85c4.379,0,7.931,3.512,7.931,7.85
|
||||||
|
C30.906,27.334,27.355,30.85,22.976,30.85z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
226
app/main.py
Normal file
226
app/main.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import os
|
||||||
|
import io
|
||||||
|
import fitz
|
||||||
|
import qrcode
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import urllib.parse
|
||||||
|
from typing import Optional
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi import FastAPI, File, UploadFile, Response
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
SERVICE_URL = os.getenv('SERVICE_URL')
|
||||||
|
DESTINATION = os.getenv('DESTINATION')
|
||||||
|
CHUNK_SIZE = int(os.getenv('CHUNK_SIZE'))
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
|
||||||
|
def urlencode(str):
|
||||||
|
return urllib.parse.quote(str)
|
||||||
|
|
||||||
|
|
||||||
|
def urldecode(str):
|
||||||
|
return urllib.parse.unquote(str)
|
||||||
|
|
||||||
|
def make_qrcode(qr_data):
|
||||||
|
"""
|
||||||
|
Generate a QR code from the given qr_data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qr_data (str): The data to be encoded in the QR code.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
io.BytesIO: The QR code image as a BytesIO object.
|
||||||
|
"""
|
||||||
|
qr = qrcode.QRCode(
|
||||||
|
version=1,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=6,
|
||||||
|
border=2,
|
||||||
|
)
|
||||||
|
qr.add_data(qr_data)
|
||||||
|
qr.make(fit=True)
|
||||||
|
|
||||||
|
img = qr.make_image(fill_color="black", back_color="white")
|
||||||
|
fp = io.BytesIO()
|
||||||
|
img.save(fp, "PNG")
|
||||||
|
return fp
|
||||||
|
|
||||||
|
def create_qr_rectangle(preset, page):
|
||||||
|
"""
|
||||||
|
Create a QR rectangle based on the preset and page provided.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
- preset (str): The preset for the QR rectangle.
|
||||||
|
- page (Page): The page on which the QR rectangle will be created.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- rect (Rect): The created QR rectangle.
|
||||||
|
"""
|
||||||
|
w = page.rect.width # page width
|
||||||
|
|
||||||
|
if preset == "inv1":
|
||||||
|
margin = 15
|
||||||
|
left = w*0.40
|
||||||
|
elif preset == "inv2":
|
||||||
|
margin = 20
|
||||||
|
left = w*0.88
|
||||||
|
else:
|
||||||
|
margin = 0
|
||||||
|
left = 0
|
||||||
|
|
||||||
|
rect = fitz.Rect(left, margin, left + 65, margin + 65)
|
||||||
|
|
||||||
|
return rect
|
||||||
|
|
||||||
|
async def chunked_copy(src, dst):
|
||||||
|
"""
|
||||||
|
Asynchronously copies the contents of the source file to the destination file
|
||||||
|
in chunks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
src: The source file object.
|
||||||
|
dst: The destination file path.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
await src.seek(0)
|
||||||
|
with open(dst, "wb") as buffer:
|
||||||
|
while True:
|
||||||
|
contents = await src.read(CHUNK_SIZE)
|
||||||
|
if not contents:
|
||||||
|
log.info(f"Src completely consumed\n")
|
||||||
|
break
|
||||||
|
log.info(f"Consumed {len(contents)} bytes from Src file\n")
|
||||||
|
buffer.write(contents)
|
||||||
|
|
||||||
|
@app.post("/signinvoice/")
|
||||||
|
async def sign_invoice(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
preset: Optional[str] = None,
|
||||||
|
code_content: Optional[str] = None,
|
||||||
|
company: Optional[str] = None,
|
||||||
|
inv_type: Optional[str] = None,
|
||||||
|
inv_series: Optional[str] = None,
|
||||||
|
inv_num: Optional[str] = None,
|
||||||
|
in_all: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Sign an invoice file and generate a QR code for it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file (UploadFile): The file to be signed.
|
||||||
|
preset (str, optional): The preset for the QR code.
|
||||||
|
code_content (str, optional): The content for the QR code.
|
||||||
|
company (str, optional): The company name.
|
||||||
|
inv_type (str, optional): The invoice type.
|
||||||
|
inv_series (str, optional): The invoice series.
|
||||||
|
inv_num (str, optional): The invoice number.
|
||||||
|
in_all (bool): Whether to include the QR code in all pages.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
FileResponse: The signed invoice file with QR code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
inv_code = "{}_{}_{}_{}".format(company, inv_type, inv_series, inv_num).replace(" ", "_").replace(",", "").replace("#", "")
|
||||||
|
inv_url = "{}/getinvoice/?inv_code={}".format(SERVICE_URL, inv_code)
|
||||||
|
|
||||||
|
#print("SIGNED URL: {}".format(inv_url))
|
||||||
|
|
||||||
|
temp_path = os.path.join(DESTINATION, f"{inv_code}_temp.pdf")
|
||||||
|
full_path = os.path.join(DESTINATION, f"{inv_code}.pdf")
|
||||||
|
|
||||||
|
await chunked_copy(file, temp_path)
|
||||||
|
|
||||||
|
if not code_content:
|
||||||
|
code_content = inv_url
|
||||||
|
|
||||||
|
qr_image = make_qrcode(code_content)
|
||||||
|
doc = fitz.open(temp_path)
|
||||||
|
|
||||||
|
add_qr = True
|
||||||
|
for page in doc:
|
||||||
|
if add_qr:
|
||||||
|
rect = create_qr_rectangle(preset, page)
|
||||||
|
if not page.is_wrapped:
|
||||||
|
page.wrap_contents()
|
||||||
|
page.insert_image(rect, stream=qr_image)
|
||||||
|
if not in_all and page.number == 0:
|
||||||
|
add_qr = False
|
||||||
|
|
||||||
|
doc.save(full_path, deflate=True, garbage=3)
|
||||||
|
|
||||||
|
if os.path.isfile(temp_path):
|
||||||
|
os.remove(temp_path)
|
||||||
|
print("{} : Returned Signed Invoice".format(datetime.datetime.now()))
|
||||||
|
return FileResponse(full_path)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/getinvoice/")
|
||||||
|
async def get_invoice(
|
||||||
|
company: Optional[str] = None,
|
||||||
|
inv_code: Optional[str] = None,
|
||||||
|
inv_type: Optional[str] = None,
|
||||||
|
inv_series: Optional[str] = None,
|
||||||
|
inv_num: Optional[str] = None,
|
||||||
|
search_file: bool = False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
An asynchronous function to retrieve an invoice file based on various parameters.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
company (Optional[str]): The company name.
|
||||||
|
inv_code (Optional[str]): The invoice code.
|
||||||
|
inv_type (Optional[str]): The invoice type.
|
||||||
|
inv_series (Optional[str]): The invoice series.
|
||||||
|
inv_num (Optional[str]): The invoice number.
|
||||||
|
search_file (bool): Flag to indicate whether to search for the file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Union[FileResponse, Dict[str, str]]: Returns the invoice file if found, otherwise returns a message.
|
||||||
|
"""
|
||||||
|
if search_file:
|
||||||
|
inv_code = "{}_{}_{}_{}".format(company, inv_type, inv_series, inv_num).replace(" ", "_").replace(",", "").replace("#", "")
|
||||||
|
|
||||||
|
if inv_code:
|
||||||
|
full_path = os.path.join(DESTINATION, "{}.pdf".format(inv_code))
|
||||||
|
if os.path.isfile(full_path):
|
||||||
|
print("FETCHING INVOICE: {} | {}".format(inv_code, full_path))
|
||||||
|
return FileResponse(full_path)
|
||||||
|
else:
|
||||||
|
print("{} : No invoice found".format(datetime.datetime.now()))
|
||||||
|
return {"message": "No invoice found"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/createqr/")
|
||||||
|
async def create_qr_code(code_content: Optional[str] = None, token: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Create a QR code from the given code_content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code_content (str, optional): The content to be encoded in the QR code. Defaults to None.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: The QR code image as a Response object with media type "image/png".
|
||||||
|
"""
|
||||||
|
if token == 'ZPOclCF5od59SgW6PLM2':
|
||||||
|
if code_content:
|
||||||
|
qr_image = make_qrcode(code_content)
|
||||||
|
print("{} : QR Created".format(datetime.datetime.now()))
|
||||||
|
return Response(content=qr_image.getvalue(), media_type="image/png")
|
||||||
|
else:
|
||||||
|
print("{} : QR Code Content not provided".format(datetime.datetime.now()))
|
||||||
|
return "QR Code Content not provided"
|
||||||
|
else:
|
||||||
|
return "Unauthorized"
|
||||||
|
|
||||||
|
|
||||||
|
@app.get('/favicon.ico', include_in_schema=False)
|
||||||
|
async def favicon():
|
||||||
|
return FileResponse("favicon.ico")
|
15
docker-compose-local.yml
Normal file
15
docker-compose-local.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
qrcode-api:
|
||||||
|
container_name: qrcode-api
|
||||||
|
image: localhost:7776/qrcodes:latest
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./invoices:/var/invoices
|
||||||
|
ports:
|
||||||
|
- 8188:80
|
||||||
|
|
||||||
|
networks:
|
||||||
|
qrcode-net:
|
||||||
|
driver: bridge
|
||||||
|
|
32
docker-compose.yml
Normal file
32
docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
version: '3.5'
|
||||||
|
services:
|
||||||
|
qrcode-api:
|
||||||
|
container_name: qrcode-api
|
||||||
|
image: git.argideli.com/quertex/qr-api:latest
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./invoices:/var/invoices
|
||||||
|
networks:
|
||||||
|
- qrcode-net
|
||||||
|
ports:
|
||||||
|
- 8188:80
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=qrcode-api-master"
|
||||||
|
|
||||||
|
qrcode-api-watchtower:
|
||||||
|
container_name: qrcode-api-watchtower
|
||||||
|
image: containrrr/watchtower
|
||||||
|
networks:
|
||||||
|
- qrcode-net
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ./config.json:/config.json
|
||||||
|
command: --interval 300 --scope api
|
||||||
|
labels:
|
||||||
|
- "com.centurylinklabs.watchtower.scope=qrcode-api-master"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
qrcode-net:
|
||||||
|
name: qrcode-net
|
||||||
|
driver: bridge
|
||||||
|
|
1
invoices/.gitignore
vendored
Normal file
1
invoices/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
*.pdf
|
18
requirements.txt
Normal file
18
requirements.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
annotated-types==0.6.0
|
||||||
|
anyio==4.2.0
|
||||||
|
click==8.1.7
|
||||||
|
fastapi==0.109.0
|
||||||
|
h11==0.14.0
|
||||||
|
idna==3.6
|
||||||
|
pydantic==2.5.3
|
||||||
|
pydantic_core==2.14.6
|
||||||
|
PyMuPDF==1.23.13
|
||||||
|
PyMuPDFb==1.23.9
|
||||||
|
pypng==0.20220715.0
|
||||||
|
python-dotenv==1.0.0
|
||||||
|
python-multipart==0.0.6
|
||||||
|
qrcode==7.4.2
|
||||||
|
sniffio==1.3.0
|
||||||
|
starlette==0.35.1
|
||||||
|
typing_extensions==4.9.0
|
||||||
|
uvicorn==0.25.0
|
6
start_dev_server.sh
Normal file
6
start_dev_server.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#/bin/bash
|
||||||
|
source ./venv/bin/activate
|
||||||
|
cd app
|
||||||
|
uvicorn main:app --reload
|
||||||
|
cd ..
|
||||||
|
deactivate
|
Loading…
x
Reference in New Issue
Block a user