├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yaml ├── k8s ├── document-template-processing │ ├── config.yaml │ ├── deployment.yaml │ └── service.yaml ├── gotenberg │ ├── deployment.yaml │ └── service.yaml └── namespace.yaml ├── main.py ├── requirements.txt ├── screenshots ├── invoice-template-doc-example.png ├── postman-test-screenshot.png ├── redoc.png └── swagger-doc.png ├── temp └── __init__.py └── utils.py /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | venv 4 | .gitignore 5 | .env.example 6 | Dockerfile 7 | .git 8 | .env 9 | README.md 10 | screenshots 11 | LICENSE 12 | docker-compose.yaml 13 | .vscode 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | GOTENBERG_API_URL= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | venv 4 | .env 5 | .vscode 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.13-slim-buster 2 | 3 | WORKDIR /code 4 | 5 | RUN python -m venv venv 6 | ENV PATH="venv/bin:$PATH" 7 | ENV GOTENBERG_API_URL=http://host.docker.internal:3000 8 | 9 | COPY ./requirements.txt /code/requirements.txt 10 | 11 | RUN pip install --no-warn-script-location \ 12 | --no-cache-dir --upgrade -r /code/requirements.txt 13 | 14 | COPY . /code 15 | 16 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] 17 | 18 | # If running behind a proxy like Nginx or Traefik add --proxy-headers 19 | # CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022, PapiHack 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Document Template Processing Service 🚀 2 | 3 | [![python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white)](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white) 4 | ![FastAPI](https://img.shields.io/badge/FastAPI-3776AB?style=for-the-badge&logo=fastapi&logoColor=white) 5 | [![docker](https://img.shields.io/badge/Docker-3776AB?style=for-the-badge&logo=docker&logoColor=white)](https://hub.docker.com/r/papihack/document-template-processor) 6 | [![kubernetes](https://img.shields.io/badge/kubernetes-3776AB?style=for-the-badge&logo=kubernetes&logoColor=white)](https://github.com/PapiHack/document-templating-service/tree/master/k8s) 7 | ![Issues](https://img.shields.io/github/issues/PapiHack/document-templating-service?style=for-the-badge&logo=appveyor) 8 | ![PR](https://img.shields.io/github/issues-pr/PapiHack/document-templating-service?style=for-the-badge&logo=appveyor) 9 | [![MIT licensed](https://img.shields.io/badge/license-mit-blue?style=for-the-badge&logo=appveyor)](./LICENSE) 10 | [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source-175x29.png?v=103)](https://github.com/ellerbrock/open-source-badges/) 11 | 12 | This is a simple and lightweight microservice that allow you to process your `Word` documents with a `templating system`, in order to hydrate it by injecting data or variables defined in it and get back its associated `PDF` result. 13 | 14 | Feel free to checkout the [official docker image](https://hub.docker.com/r/papihack/document-template-processor) on my docker hub. 15 | 16 | ## Notes 17 | 18 | For transforming the `docx` result to `PDF` you'll need have an instance of [Gotenberg](https://gotenberg.dev) up and running and provide its `ROOT URL` as an environment variable to the microservice. 19 | 20 | ## Installation 21 | 22 | - First and foremost you'll need to have an instance of Gotenberg. You can easily have one by running the following docker command : 23 | 24 | docker run --name gotenberg -d -p 3000:3000 gotenberg/gotenberg:7 25 | 26 | 27 | - After this step done, you have the two (2) options in order to run this service : 28 | 29 | - Direct run by following the steps below : 30 | 31 | - Rename the file `.env.example` at the root level of this project by simply `.env` 32 | 33 | - Set the value of the `GOTENBERG_API_URL` environment variable defined in it with yours (in our example => http://localhost:3000) 34 | 35 | - Install the necessary dependencies by running : 36 | 37 | pip install -r requirements.txt (you can create virtual env if you want it) 38 | 39 | - Start the microservice by running: 40 | 41 | uvicorn main:app 42 | 43 | - Visit the `API Documentation` at or 44 | 45 | - Using docker to build and launch a container : 46 | 47 | - In your terminal, at the root of this project, build an image of this service by running : 48 | 49 | docker build -t document-template-processing-service . 50 | 51 | - Start the service by creating a container with : 52 | 53 | docker run -d -p 8000:8000 -e GOTENBERG_API_URL=your-host-ip:3000 --name dtps document-template-processing-service 54 | 55 | - An official image is also available on my [docker hub](https://hub.docker.com/r/papihack/document-template-processor), so you can simply run 56 | the following command and skip the build step if you want too : 57 | 58 | docker run -d -p 8000:8000 -e GOTENBERG_API_URL=your-host-ip:3000 --name dtps papihack/document-template-processor 59 | 60 | - Visit the `API Documentation` at or 61 | 62 | 63 | ## Up & Running with Docker Compose 64 | 65 | If you are lazy like me 😄, you can setup the project by just running the following command : 66 | 67 | docker-compose up -d 68 | 69 | And stop all the service with : 70 | 71 | docker-compose stop 72 | 73 | Or stop and remove all the service with : 74 | 75 | docker-compose down 76 | 77 | 78 | ## Up & Running with Kubernetes 79 | 80 | If you want to deploy this project on your kubernetes cluster, you can inspect and/or edit the manifest files available in the 81 | `k8s` directory before apply them. 82 | 83 | Start by creating the namespace named `utils` by running : 84 | 85 | kubectl apply -f k8s/namespace.yaml 86 | 87 | Then, you can deploy the necessary components by running : 88 | 89 | kubectl apply -f k8s/gotenberg -f k8s/document-template-processing 90 | 91 | After that, feel free to create an `ingress` for `svc/document-template-processing` if you are using such kind of k8s component. 92 | 93 | Otherwise, you can port forward the api service by running the following command before visiting : 94 | 95 | kubectl port-forward svc/document-template-processing 8000:8000 -n utils 96 | 97 | 98 | ## Usage 99 | 100 | For now, you have an endpoint named `/api/v1/process-template-document` that will allow you to make a `POST HTTP REQUEST` by sending two (2) required parameters : 101 | 102 | - The `file` parameter that contains your `Word Document Template` 103 | 104 | - The `data` parameter that is a `JSON object` with data or variable that we are going to inject in the `file` parameter 105 | 106 | As a response, you'll get back its corresponding `PDF` file as a result. 107 | 108 | ### Example 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
Template ExamplePostman Test Result
120 | 121 | ## 🚨 Cautions for Cloud Run 🚨 122 | 123 | If you plan to run this on cloud, please change the `gotenberg` docker image with `gotenberg/gotenberg:7-cloudrun` or `thecodingmachine/gotenberg:7-cloudrun` in `docker-compose.yaml` file. This will allow you to save cost and so on. 124 | 125 | For more infos, please look at [Gotenberg Docs](https://gotenberg.dev/docs/get-started/cloud-run). 126 | 127 | 128 | ### Screenshots 129 | 130 | ![screenshot](./screenshots/swagger-doc.png) 131 | 132 | ## Contributing 133 | 134 | Feel free to make a PR or report an issue 😃 135 | 136 | Oh, one more thing, please do not forget to put a description when you make your PR 🙂 137 | 138 | ## Author 139 | 140 | - [M.B.C.M](https://github.com/PapiHack) 141 | [![My Twitter Link](https://img.shields.io/twitter/follow/the_it_dev?style=social)](https://twitter.com/the_it_dev) 142 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | gotenberg: 4 | container_name: gotenberg-service 5 | # replace image with gotenberg/gotenberg:7-cloudrun or thecodingmachine/gotenberg:7-cloudrun 6 | # if you plan to run it on cloud 7 | image: gotenberg/gotenberg:7 8 | restart: always 9 | ports: 10 | - "3000:3000" 11 | api: 12 | container_name: document-template-processor-service 13 | build: . 14 | restart: always 15 | depends_on: 16 | - gotenberg 17 | ports: 18 | - "8000:8000" 19 | environment: 20 | - GOTENBERG_API_URL=http://gotenberg-service:3000 21 | -------------------------------------------------------------------------------- /k8s/document-template-processing/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: document-template-processing-config 5 | namespace: utils 6 | data: 7 | gotenberg-service-url: http://gotenberg.utils.svc 8 | -------------------------------------------------------------------------------- /k8s/document-template-processing/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: document-template-processing 5 | namespace: utils 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: document-template-processing 11 | template: 12 | metadata: 13 | labels: 14 | app: document-template-processing 15 | spec: 16 | containers: 17 | - name: document-template-processing 18 | image: papihack/document-template-processor 19 | env: 20 | - name: GOTENBERG_API_URL 21 | valueFrom: 22 | configMapKeyRef: 23 | name: document-template-processing-config 24 | key: gotenberg-service-url 25 | resources: 26 | limits: 27 | memory: "256Mi" 28 | cpu: "800m" 29 | ports: 30 | - name: web 31 | containerPort: 8000 32 | -------------------------------------------------------------------------------- /k8s/document-template-processing/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: document-template-processing 5 | namespace: utils 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: document-template-processing 10 | ports: 11 | - name: http 12 | port: 8000 13 | targetPort: web 14 | -------------------------------------------------------------------------------- /k8s/gotenberg/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: gotenberg 5 | namespace: utils 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: gotenberg 11 | template: 12 | metadata: 13 | labels: 14 | app: gotenberg 15 | spec: 16 | containers: 17 | - name: gotenberg 18 | securityContext: 19 | privileged: false 20 | runAsUser: 1001 21 | image: gotenberg/gotenberg:7-cloudrun 22 | resources: 23 | limits: 24 | memory: "512Mi" 25 | cpu: "800m" 26 | ports: 27 | - name: web 28 | containerPort: 3000 29 | -------------------------------------------------------------------------------- /k8s/gotenberg/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: gotenberg 5 | namespace: utils 6 | spec: 7 | type: ClusterIP 8 | selector: 9 | app: gotenberg 10 | ports: 11 | - port: 80 12 | name: http 13 | targetPort: web 14 | -------------------------------------------------------------------------------- /k8s/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: utils 5 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Body, FastAPI, File, UploadFile 2 | from fastapi.responses import FileResponse, JSONResponse 3 | from pydantic import Json 4 | from docxtpl import DocxTemplate 5 | import aiofiles 6 | from utils import remove_temporary_files, get_env 7 | import requests 8 | 9 | app = FastAPI( 10 | title="Document Template Processing Service", 11 | description=""" 12 | This is the documentation of the REST API exposed by the document template processing microservice. 13 | This will allow you to inject data in a specific word document template and get the pdf format as a result. 🚀🚀🚀 14 | """, 15 | version="1.0.0" 16 | ) 17 | 18 | SERVICE_STATUS = {'status': 'Service is healthy !'} 19 | 20 | @app.get('/') 21 | async def livenessprobe(): 22 | remove_temporary_files() 23 | return SERVICE_STATUS 24 | 25 | @app.get('/health-check') 26 | async def healthcheck(): 27 | remove_temporary_files() 28 | return SERVICE_STATUS 29 | 30 | @app.post('/api/v1/process-template-document') 31 | async def process_document_template(data: Json = Body(...), file: UploadFile = File(...)): 32 | if file.filename == '': 33 | return JSONResponse({'status': 'error', 'message': 'file is required'}, status_code=400) 34 | if data is None or len(data) == 0: 35 | return JSONResponse({'status': 'error', 'message': 'data is required'}, status_code=400) 36 | resourceURL = '{}/forms/libreoffice/convert'.format(get_env('GOTENBERG_API_URL')) 37 | file_path = 'temp/{}'.format(file.filename) 38 | pdf_file_path = 'temp/{}.pdf'.format(file.filename.split('.')[0]) 39 | async with aiofiles.open(file_path, 'wb') as out_file: 40 | while content := await file.read(1024): 41 | await out_file.write(content) 42 | document = DocxTemplate(file_path) 43 | document.render(data) 44 | document.save(file_path) 45 | response = requests.post(url=resourceURL, files={'file': open(file_path, 'rb')}) 46 | async with aiofiles.open(pdf_file_path, 'wb') as out_file: 47 | await out_file.write(response.content) 48 | return FileResponse(pdf_file_path, media_type='application/pdf') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.8.0 2 | anyio==3.5.0 3 | asgiref==3.5.0 4 | certifi==2021.10.8 5 | charset-normalizer==2.0.12 6 | click==8.0.4 7 | docxcompose==1.3.4 8 | docxtpl==0.15.2 9 | fastapi==0.75.0 10 | h11==0.13.0 11 | httptools==0.4.0 12 | idna==3.3 13 | Jinja2==3.0.3 14 | lxml==4.8.0 15 | MarkupSafe==2.1.1 16 | pydantic==1.9.0 17 | python-docx==0.8.11 18 | python-dotenv==0.19.2 19 | python-multipart==0.0.5 20 | PyYAML==6.0 21 | requests==2.27.1 22 | six==1.16.0 23 | sniffio==1.2.0 24 | starlette==0.17.1 25 | tqdm==4.63.0 26 | typing-extensions==4.1.1 27 | urllib3==1.26.9 28 | uvicorn==0.17.6 29 | uvloop==0.16.0 30 | watchgod==0.8 31 | websockets==10.2 32 | -------------------------------------------------------------------------------- /screenshots/invoice-template-doc-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PapiHack/document-templating-service/adb593da4b9395db113b7d52a3189e526227fe4b/screenshots/invoice-template-doc-example.png -------------------------------------------------------------------------------- /screenshots/postman-test-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PapiHack/document-templating-service/adb593da4b9395db113b7d52a3189e526227fe4b/screenshots/postman-test-screenshot.png -------------------------------------------------------------------------------- /screenshots/redoc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PapiHack/document-templating-service/adb593da4b9395db113b7d52a3189e526227fe4b/screenshots/redoc.png -------------------------------------------------------------------------------- /screenshots/swagger-doc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PapiHack/document-templating-service/adb593da4b9395db113b7d52a3189e526227fe4b/screenshots/swagger-doc.png -------------------------------------------------------------------------------- /temp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PapiHack/document-templating-service/adb593da4b9395db113b7d52a3189e526227fe4b/temp/__init__.py -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import dotenv_values 3 | 4 | env = dotenv_values('./.env') 5 | 6 | def get_env(var_name: str) -> str: 7 | if 'GOTENBERG_API_URL' in os.environ: 8 | return os.environ['GOTENBERG_API_URL'] 9 | return env[var_name] 10 | 11 | def remove_file(filename: str): 12 | file_path = 'temp/{}'.format(filename) 13 | if os.path.isfile(file_path): 14 | os.remove(file_path) 15 | print('{} successfully removed!'.format(file_path)) 16 | else: 17 | print('Error: {} file not found'.format(file_path)) 18 | 19 | def remove_temporary_files(): 20 | dir_name = 'temp/' 21 | files = os.listdir(dir_name) 22 | 23 | if len(files) > 1: 24 | for item in files: 25 | if item.endswith('.docx') or item.endswith('.doc') or item.endswith('.pdf'): 26 | os.remove(os.path.join(dir_name, item)) 27 | 28 | --------------------------------------------------------------------------------