Skip to content

Commit

Permalink
0.57.0 Release (#131)
Browse files Browse the repository at this point in the history
* Implement Fast API optional component (#117)

* added Fast API optional component

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* updating version number

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* updating CI to test on Python 3.9 and 3.10 (#118)

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* X12 Image Build (#121)

* adding Dockerfile for image build

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* updating GitHub workflows

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* updating image ci to use build-args

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* configuring PR based image CI to build a single image to reduce wait times

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* updating image build ci

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* removing Dockerfile path condition since it's not working

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* Updated container support documentation (#127)

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* update /x12 [POST] endpoint segment mode to return an equivalent response as the CLI in segment mode (#129)

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* adding script to build and push multi-platform images (#130)

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>

* removing tag version from GitHub actions

Signed-off-by: Dixon Whitmire <dixonwh@gmail.com>
  • Loading branch information
dixonwhitmire committed Jun 10, 2022
1 parent 3734589 commit c97b41a
Show file tree
Hide file tree
Showing 13 changed files with 421 additions and 7 deletions.
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.github
.idea
.vscode
**/.pytest_cache
**/linuxforhealth_x12.egg-info
demo-file
repo-docs
**/tests
venv
.gitignore
LICENSE
4 changes: 2 additions & 2 deletions .github/workflows/continuous-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9]
python-version: ["3.9.13", "3.10.5"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -22,7 +22,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip setuptools
pip install -e .[dev]
pip install -e .[dev,api]
- name: Validate Formatting
run: |
black -t py38 --check --diff ./src
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/test-image-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Test X12 Image Build

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
build-image:
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v3
- name: QEMU setup
uses: docker/setup-qemu-action@v2
- name: Docker buildx setup
uses: docker/setup-buildx-action@v2
- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
build-args: |
X12_SEM_VER=0.57.0
platforms: linux/amd64
push: false
tags: ci-testing
49 changes: 49 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Builds the LinuxForHealth X12 API container using a multi-stage build

# build stage
FROM python:3.10-slim-buster AS builder

# the full semantic version number, used to match to the generated wheel file in dist/
ARG X12_SEM_VER

# OS library updates and build tooling
RUN apt-get update
RUN apt-get install -y --no-install-recommends \
build-essential \
gcc

# copy source and build files
WORKDIR /tmp/lfh-x12
COPY setup.cfg .
COPY pyproject.toml .
COPY ../src src/

# build the service
RUN python -m venv /tmp/lfh-x12/venv
ENV PATH="/tmp/lfh-x12/venv/bin:$PATH"
RUN python -m pip install --upgrade pip setuptools wheel build
RUN python -m build
RUN python -m pip install dist/linuxforhealth_x12-"$X12_SEM_VER"-py3-none-any.whl[api]

# main image
FROM python:3.10-slim-buster

# container build arguments
# lfh user id and group ids
ARG LFH_USER_ID=1000
ARG LFH_GROUP_ID=1000

# create service user and group
RUN groupadd -g $LFH_GROUP_ID lfh && \
useradd -m -u $LFH_USER_ID -g lfh lfh
USER lfh
WORKDIR /home/lfh

# configure and execute application
COPY --from=builder /tmp/lfh-x12/venv ./venv
# set venv executables first in path
ENV PATH="/home/lfh/venv/bin:$PATH"
# listening address for application
ENV X12_UVICORN_HOST=0.0.0.0
EXPOSE 5000
CMD ["python", "-m", "linuxforhealth.x12.api"]
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ git clone https://github.com/LinuxForHealth/x12
cd x12

python3 -m venv venv && source venv/bin/activate && pip install --upgrade pip setuptools
pip install -e .[dev]
pip install -e .[dev, api] # installs dev packages and optional API endpoint
pytest
```

Expand Down Expand Up @@ -130,6 +130,27 @@ To parse a X12 message into models with pretty printing enabled
In "model" mode, the `-x` option excludes `None` values from output.
### API
LinuxForHealth X12 includes an experimental "api" setup "extra" which activates a [Fast API](https://fastapi.tiangolo.com/)
endpoint used to submit X12 payloads.
```shell
user@mbp x12 % source venv/bin/activate
(venv) user@mbp x12 % pip install -e ".[api]"
(venv) user@mbp x12 % lfhx12-api
```
Browse to http://localhost:5000/docs to view the Open API UI.
API server configurations are located in the [config module](./src/linuxforhealth/x12/config.py). The `X12ApiConfig` model
is a [Pydantic Settings Model](https://pydantic-docs.helpmanual.io/usage/settings/) which can be configured using environment
variables.
```shell
user@mbp x12 % source venv/bin/activate
(venv) user@mbp x12 % export X12_UVICORN_PORT=5002
(venv) user@mbp x12 % lfhx12-api
```
### Code Formatting
LinuxForHealth X12 adheres to the [Black Code Style and Convention](https://black.readthedocs.io/en/stable/index.html)
Expand Down Expand Up @@ -161,3 +182,4 @@ python3 -m build --no-isolation
## Additional Resources
- [Design Overview](repo-docs/DESIGN.md)
- [New Transaction Support](repo-docs/NEW_TRANSACTION.md)
- [Container Support](repo-docs/CONTAINER_SUPPORT.md)
1 change: 1 addition & 0 deletions demo-file/demo-single-line.270
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ISA*03*9876543210*01*9876543210*30*000000005 *30*12345 *131031*1147*^*00501*000000907*1*T*:~GS*HS*000000005*54321*20131031*1147*1*X*005010X279A1~ST*270*1234*005010X279A1~BHT*0022*13*10001234*20060501*1319~HL*1**20*1~NM1*PR*2*ABC COMPANY*****PI*842610001~HL*2*1*21*1~NM1*1P*2*BONE AND JOINT CLINIC*****SV*2000035~HL*3*2*22*0~TRN*1*93175-012547*9877281234~NM1*IL*1*SMITH*ROBERT****MI*11122333301~DMG*D8*19430519~DTP*291*D8*20060501~EQ*30~SE*13*1234~GE*1*1~IEA*1*000000907~
38 changes: 38 additions & 0 deletions push-image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash
# push-image.sh
# push-image.sh builds and pushes the multi-platform linuxforhealth/x12 image to an image repository.
#
# Pre-Requisites:
# - The script environment is authenticated to the target image repository.
# - The project has been built and a wheel exists in <project root>/dist.
#
# Usage:
# ./push-image [image_tag] [image_url] [platforms]
#
# Positional Script arguments:
# IMAGE_TAG - Aligns with the project's semantic version. Required.
# IMAGE_URL - The image's URL. Defaults to ghcr.io/linuxforhealth/x12.
# PLATFORMS - String containing the list of platforms. Defaults to linux/amd64,linux/arm64,linux/s390x.

set -o errexit
set -o nounset
set -o pipefail

if [[ $# == 0 ]]
then
echo "Missing required argument IMAGE_TAG"
echo "Usage: ./push-image.sh [image tag] [image url] [platforms]"
exit 1;
fi

IMAGE_TAG=$1
IMAGE_URL="${2:-ghcr.io/linuxforhealth/x12}"
PLATFORMS="${3:-linux/amd64,linux/arm64,linux/s390x}"

docker buildx build \
--pull \
--push \
--platform "$PLATFORMS" \
--build-arg X12_SEM_VER="$IMAGE_TAG" \
--tag "$IMAGE_URL":"$IMAGE_TAG" \
--tag "$IMAGE_URL":latest .
43 changes: 43 additions & 0 deletions repo-docs/CONTAINER_SUPPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# LinuxForHealth X12 Container Support

The LinuxForHealth X12 API component supports a containerized execution environment. This guide provides an overview of
how to build and run the image.

## Image Build

### Supported Build Arguments


| Build Argument | Description | Default Value |
|----------------|------------------------------------------------|---------------|
| X12_SEM_VER | The current X12 library sematic version number | None |
| LFH_USER_ID | The user id used for the LFH container user | 1000 |
| LFH_GROUP_ID | The group id used for the LFH container group | 1000 |

The `X12_SEM_VER`. This argument should align with the current `linuxforhealth.x12.__version__` attribute value and the
desired image tag.

```shell
docker build --build-arg X12_SEM_VER=0.57.0 -t x12:0.57.0 .
```

## Run Container

### Supported Environment Configurations

| Build Argument | Description | Default Value |
|------------------|-----------------------------------|---------------|
| X12_UVICORN_HOST | The container's listening address | 0.0.0.0 |


The following command launches the LinuxForHealth X12 container:
```shell
docker run --name lfh-x12 --rm -d -p 5000:5000 ghcr.io/linuxforhealth/x12:latest
```

To access the Open API UI, browse to http://localhost:5000/docs

Finally, to stop and remove the container:
```shell
docker stop lfh-x12
```
6 changes: 3 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ classifiers =
Intended Audience :: Developers

[options]
include_package_data = True
install_requires =
pydantic >= 1.9
python-dotenv >= 0.19.0
Expand All @@ -32,7 +31,8 @@ where=src
console_scripts =
lfhx12 = linuxforhealth.x12.cli:main
black = black:patched_main
lfhx12-api = linuxforhealth.x12.api:run_server

[options.extras_require]
api = fastapi; uvicorn[standard]
dev = black>=21.8b0; pre-commit>=2.14.1;pytest>=6.2.5
api = fastapi>=0.78.0; uvicorn[standard]>=0.17.0; requests>=2.27.0
dev = black>=22.3.0; pre-commit>=2.14.1;pytest>=7.1.0
2 changes: 1 addition & 1 deletion src/linuxforhealth/x12/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

load_dotenv()

__version__ = "0.56.02"
__version__ = "0.57.0"
104 changes: 104 additions & 0 deletions src/linuxforhealth/x12/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from fastapi import FastAPI, Header, HTTPException, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
import uvicorn
from typing import Dict, Optional, List
from linuxforhealth.x12.config import get_x12_api_config, X12ApiConfig
from linuxforhealth.x12.io import X12SegmentReader, X12ModelReader
from linuxforhealth.x12.parsing import X12ParseException
from pydantic import ValidationError, BaseModel, Field

app = FastAPI()


@app.exception_handler(RequestValidationError)
async def request_validation_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content=jsonable_encoder(
{"detail": "Invalid request. Expected {'x12': <x12 message string>}"}
),
)


class X12Request(BaseModel):
"""
The X12 Request object
"""

x12: str = Field(description="The X12 payload to process, conveyed as a string")

class Config:
schema_extra = {
"example": {
"x12": "ISA*03*9876543210*01*9876543210*30*000000005 *30*12345 *131031*1147*^*00501*000000907*1*T*:~GS*HS*000000005*54321*20131031*1147*1*X*005010X279A1~ST*270*1234*005010X279A1~BHT*0022*13*10001234*20060501*1319~HL*1**20*1~NM1*PR*2*ABC COMPANY*****PI*842610001~HL*2*1*21*1~NM1*1P*2*BONE AND JOINT CLINIC*****SV*2000035~HL*3*2*22*0~TRN*1*93175-012547*9877281234~NM1*IL*1*SMITH*ROBERT****MI*11122333301~DMG*D8*19430519~DTP*291*D8*20060501~EQ*30~SE*13*1234~GE*1*1~IEA*1*000000907~",
}
}


@app.post("/x12")
async def post_x12(
x12_request: X12Request,
lfh_x12_response: Optional[str] = Header(default="models"),
) -> List[Dict]:
"""
Processes an incoming X12 payload.
Requests are submitted as:
{
"x12": <x12 message string>
}
The response payload is a JSON document containing either the "raw" X12 segments, or a rich
X12 domain model. The response type defaults to the domain model and is configured using the
LFH-X12-RESPONSE header. Valid values include: "segments" or "models".
:param x12_request: The X12 request model/object.
:param lfh_x12_response: A header value used to drive processing.
:return: The X12 response - List[List] (segments) or List[Dict] (models)
"""
if lfh_x12_response.lower() not in ("models", "segments"):
lfh_x12_response = "models"

try:
if lfh_x12_response.lower() == "models":
with X12ModelReader(x12_request.x12) as r:
api_results = [m.dict() for m in r.models()]
else:
with X12SegmentReader(x12_request.x12) as r:
api_results = []
for segment_name, segment in r.segments():
segment_data = {
f"{segment_name}{str(i).zfill(2)}": v
for i, v in enumerate(segment)
}
api_results.append(segment_data)

except (X12ParseException, ValidationError) as error:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid X12 payload. To troubleshoot please run the LFH X12 CLI",
)
else:
return api_results


def run_server():
"""Launches the API server"""
config: X12ApiConfig = get_x12_api_config()

uvicorn_params = {
"app": config.x12_uvicorn_app,
"host": config.x12_uvicorn_host,
"port": config.x12_uvicorn_port,
"reload": config.x12_uvicorn_reload,
}

uvicorn.run(**uvicorn_params)


if __name__ == "__main__":
run_server()
23 changes: 23 additions & 0 deletions src/linuxforhealth/x12/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,26 @@ class Config:
def get_config() -> "X12Config":
"""Returns the X12Config"""
return X12Config()


class X12ApiConfig(BaseSettings):
"""
Settings for optional Fast API "server" components.
"""

x12_uvicorn_app: str = Field(
"linuxforhealth.x12.api:app", description="The path the ASGI app object"
)
x12_uvicorn_host: str = Field(
"0.0.0.0", description="The ASGI listening address (host)"
)
x12_uvicorn_port: int = Field(5000, description="The ASGI listening port (host)")
x12_uvicorn_reload: bool = Field(
False, description="Set to True to support hot reloads. Defaults to False"
)


@lru_cache
def get_x12_api_config() -> "X12ApiConfig":
"""Returns the X12ApiConfig"""
return X12ApiConfig()
Loading

0 comments on commit c97b41a

Please sign in to comment.