Skip to content

Feature/production ready #502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ jobs:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- name: Build the Docker image
run: make compose/rebuild
- name: Run static checks in Docker image
- name: Lint in Docker image
run: make compose/lint
- name: Run test in Docker image
- name: Test in Docker image
run: make compose/test
- name: Run in Docker image
run: make compose/run
- name: Tag Docker image
run: >
Expand Down
58 changes: 43 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ FROM python:3.12.4-alpine3.20 AS base
ENV WORKDIR=/app
WORKDIR ${WORKDIR}

RUN apk add --update --no-cache make

###############################################################################
FROM base AS lint

Expand Down Expand Up @@ -35,34 +37,51 @@ COPY ./requirements.txt ${WORKDIR}/
COPY ./setup.cfg ${WORKDIR}/
COPY ./Makefile ${WORKDIR}/

# code linting conf
COPY ./.pylintrc ${WORKDIR}/
COPY ./.coveragerc ${WORKDIR}/
COPY ./setup.cfg ${WORKDIR}/

# markdownlint conf
COPY ./.markdownlint.yaml ${WORKDIR}/

# yamllint conf
COPY ./.yamllint ${WORKDIR}/
COPY ./.yamlignore ${WORKDIR}/

# pylint and covergae
COPY ./.pylintrc ${WORKDIR}/
COPY ./.coveragerc ${WORKDIR}/

CMD ["make", "lint"]

###############################################################################
FROM base AS development

RUN apk add --update --no-cache make
COPY ./Makefile ${WORKDIR}/
COPY ./requirements.txt ${WORKDIR}/
COPY ./setup.cfg ${WORKDIR}/

###############################################################################
FROM development AS builder
RUN make dependencies

COPY ./src ${WORKDIR}/src
COPY ./requirements.txt ${WORKDIR}/
COPY ./Makefile ${WORKDIR}/
COPY ./setup.cfg ${WORKDIR}/

RUN ls -alh

RUN pip install -r requirements.txt
# CMD []

###############################################################################
FROM development AS builder

ENV WORKDIR=/app
WORKDIR ${WORKDIR}

RUN apk add --update --no-cache rsync

RUN rsync -av --prune-empty-dirs \
--exclude '*_test.py' \
--exclude '*.pyc' \
--exclude '.venv' \
--exclude '__pycache__' \
src/ build/

# CMD []

###############################################################################
### In testing stage, can't use USER, due permissions issue
Expand All @@ -78,27 +97,36 @@ ENV BRUTEFORCE=false
WORKDIR /app

COPY ./.coveragerc ${WORKDIR}/
COPY ./setup.cfg ${WORKDIR}/

RUN ls -alh

CMD ["make", "test", "-e", "{DEBUG}"]
CMD ["make", "test"]

###############################################################################
### In production stage
## in the production phase, "good practices" such as
## WORKDIR and USER are maintained
##
FROM builder AS production
FROM python:3.12.4-alpine3.20 AS production

ENV LOG_LEVEL=INFO
ENV BRUTEFORCE=false
ENV WORKDIR=/app
WORKDIR ${WORKDIR}

RUN adduser -D worker
RUN mkdir -p /app
RUN chown worker:worker /app

WORKDIR /app
RUN apk add --update --no-cache make
COPY ./Makefile ${WORKDIR}/

COPY --from=builder /app/build/ ${WORKDIR}/

RUN ls -alh

USER worker
CMD ["make", "test", "-e", "{DEBUG}"]
CMD ["make", "run"]

# checkov:skip= CKV_DOCKER_2: production image isn't a service process (yet)
22 changes: 21 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ test/static: dependencies
test/styling: dependencies
${RUNTIME_TOOL} -m pycodestyle --statistics src/

format:
${RUNTIME_TOOL} -m autopep8 --in-place --recursive --aggressive --aggressive --verbose src/

build: env
rsync -av --prune-empty-dirs \
--exclude '*_test.py' \
--exclude '*.pyc' \
--exclude '.venv' \
--exclude '__pycache__' \
src/ build/

test: env dependencies
${RUNTIME_TOOL} -m coverage run -m \
pytest --verbose \
Expand Down Expand Up @@ -110,17 +121,20 @@ clean:
rm -fr .pytest_cache
rm -fr htmlcov
rm -fr coverage
rm -fr build
find . -path "*/*.pyc" -delete -print
find . -path "*/*.pyo" -delete -print
find . -path "*/__pycache__" -type d -print -exec rm -fr {} ';'

compose/build: env
docker-compose --profile lint build
docker-compose --profile testing build
docker-compose --profile production build

compose/rebuild: env
docker-compose --profile lint build --no-cache
docker-compose --profile testing build --no-cache
docker-compose --profile production build --no-cache

compose/lint/markdown: compose/build
docker-compose --profile lint run --rm algorithm-exercises-py-lint make lint/markdown
Expand All @@ -136,7 +150,13 @@ compose/test/static: compose/build

compose/lint: compose/lint/markdown compose/lint/yaml compose/test/styling compose/test/static

compose/test: compose/build
docker-compose --profile testing run --rm algorithm-exercises-py-test make test

compose/run: compose/build
docker-compose --profile testing run --rm algorithm-exercises-py make test
docker-compose --profile production run --rm algorithm-exercises-py make run

all: lint coverage

run:
ls -alh
14 changes: 12 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---

services:
algorithm-exercises-py:
image: algorithm-exercises-py:latest
algorithm-exercises-py-test:
image: algorithm-exercises-py:test
build:
context: .
target: testing
Expand Down Expand Up @@ -37,6 +37,16 @@ services:
- ./:/app
profiles: ["development"]

algorithm-exercises-py:
image: algorithm-exercises-py:latest
build:
context: .
target: production
environment:
LOG_LEVEL: ${LOG_LEVEL:-INFO} ## (1) ## INFO | DEBUG
BRUTEFORCE: ${BRUTEFORCE:-false} ## (1) ## true | false
profiles: ["production"]

## REFERENCES:
## (1) Passing Environment variable with fallback value:
## https://stackoverflow.com/a/70772707/6366150
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def array_manipulation(n: int, queries: list[list[int]]) -> int:
for [a, b, k] in queries:

LOGGER.debug("start -> %s", result)
for j in range(a, b+1):
for j in range(a, b + 1):
result[j] += k
LOGGER.debug("result -> %s", result)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def array_manipulation_optimized(n: int, queries: list[list[int]]) -> int:
for [a, b, k] in queries:
# Prefix
result[a] += k
result[b+1] -= k
result[b + 1] -= k

accum_sum = 0
for value in result:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
CRUCH_SMALL_TEST_CASES = tests = [
{
'title': "Sample Test Case 0",
'n': 5,
'queries': [[1, 2, 100],
[2, 5, 100],
[3, 4, 100]],
'answer': 200
},
{
'title': "Sample Test Case 1",
'n': 10,
'queries': [[1, 5, 3],
[4, 8, 7],
[6, 9, 1]],
'answer': 10
},
{
'title': "Sample Test Case 3",
'n': 10,
'queries': [[2, 6, 8],
[3, 5, 7],
[1, 8, 1],
[5, 9, 15]],
'answer': 31
},
]
{
'title': "Sample Test Case 0",
'n': 5,
'queries': [[1, 2, 100],
[2, 5, 100],
[3, 4, 100]],
'answer': 200
},
{
'title': "Sample Test Case 1",
'n': 10,
'queries': [[1, 5, 3],
[4, 8, 7],
[6, 9, 1]],
'answer': 10
},
{
'title': "Sample Test Case 3",
'n': 10,
'queries': [[2, 6, 8],
[3, 5, 7],
[1, 8, 1],
[5, 9, 15]],
'answer': 31
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ class TestArrayLeftRotation(unittest.TestCase):
def test_rot_left_one(self):

tests = [
{'input': [1, 2, 3, 4, 5], 'answer': [2, 3, 4, 5, 1]},
{'input': [2, 3, 4, 5, 1], 'answer': [3, 4, 5, 1, 2]},
{'input': [3, 4, 5, 1, 2], 'answer': [4, 5, 1, 2, 3]},
{'input': [4, 5, 1, 2, 3], 'answer': [5, 1, 2, 3, 4]},
{'input': [5, 1, 2, 3, 4], 'answer': [1, 2, 3, 4, 5]},
{'input': [1, 2, 3, 4, 5], 'answer': [2, 3, 4, 5, 1]},
{'input': [2, 3, 4, 5, 1], 'answer': [3, 4, 5, 1, 2]},
{'input': [3, 4, 5, 1, 2], 'answer': [4, 5, 1, 2, 3]},
{'input': [4, 5, 1, 2, 3], 'answer': [5, 1, 2, 3, 4]},
{'input': [5, 1, 2, 3, 4], 'answer': [1, 2, 3, 4, 5]},
]

for _, _tt in enumerate(tests):
Expand All @@ -24,7 +24,7 @@ def test_rot_left_one(self):
def test_rot_left(self):

tests = [
{'input': [1, 2, 3, 4, 5], 'd': 4, 'answer': [5, 1, 2, 3, 4]},
{'input': [1, 2, 3, 4, 5], 'd': 4, 'answer': [5, 1, 2, 3, 4]},
]

for _, _tt in enumerate(tests):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


def minimum_swaps(group: list[int]) -> int:
q = [i-1 for i in group]
q = [i - 1 for i in group]

swaps = 0
index = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class TestMinimumSwaps(unittest.TestCase):
def test_minimum_swaps(self):

tests = [
{'title': 'Sample input 0', 'input': [4, 3, 1, 2], 'answer': 3},
{'title': 'Sample input 1', 'input': [2, 3, 4, 1, 5], 'answer': 3},
{'title': 'Sample input 2', 'input': [1, 3, 5, 2, 4, 6, 7], 'answer': 3},
{'title': 'Sample input 0', 'input': [4, 3, 1, 2], 'answer': 3},
{'title': 'Sample input 1', 'input': [2, 3, 4, 1, 5], 'answer': 3},
{'title': 'Sample input 2', 'input': [1, 3, 5, 2, 4, 6, 7], 'answer': 3},
]

for _, _tt in enumerate(tests):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ def test_minimum_bribes(self):

tests = [
{'title': "Test Case 0-0",
'input': [2, 1, 5, 3, 4], 'answer': 3},
'input': [2, 1, 5, 3, 4], 'answer': 3},
{'title': "Test Case 0-1",
'input': [2, 5, 1, 3, 4], 'answer': TOO_CHAOTIC_ERROR},
'input': [2, 5, 1, 3, 4], 'answer': TOO_CHAOTIC_ERROR},
{'title': "Test Case 1-1",
'input': [5, 1, 2, 3, 7, 8, 6, 4], 'answer': TOO_CHAOTIC_ERROR},
'input': [5, 1, 2, 3, 7, 8, 6, 4], 'answer': TOO_CHAOTIC_ERROR},
{'title': "Test Case 1-2",
'input': [1, 2, 5, 3, 7, 8, 6, 4], 'answer': 7},
'input': [1, 2, 5, 3, 7, 8, 6, 4], 'answer': 7},
{'title': "Test Case 2",
'input': [1, 2, 5, 3, 4, 7, 8, 6], 'answer': 4},
'input': [1, 2, 5, 3, 4, 7, 8, 6], 'answer': 4},
]

for _, _tt in enumerate(tests):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def count_triplets_brute_force(arr: list[int], r: int) -> int:

for i in range(0, size - 2):
for j in range(i + 1, size - 1):
for k in range(j+1, size):
for k in range(j + 1, size):
print(arr[i], arr[j], arr[k])

if r * arr[i] == arr[j] and r * arr[j] == arr[k]:
Expand All @@ -23,8 +23,8 @@ def count_triplets(arr, r):
triplets = 0

for i in arr:
j = i//r
k = i*r
j = i // r
k = i * r
a[i] -= 1
if b[j] and a[k] and i % r == 0:
triplets += b[j] * a[k]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def max_array_sum(arr_input: list[int]):
arr[1] = t_max

for i in range(2, n):
t_max = max(arr[i-2] + arr[i], t_max) # Max uptill now
t_max = max(arr[i - 2] + arr[i], t_max) # Max uptill now
t_max = max(arr[i], t_max) # Max in special case where
# arr[i] + previous max is still less than arr[i]
# update our inplace array with max for future calculations
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class RoadsAndLibraries:

def __init__(self, n: int, cities: list[list[int]]):

self._paths = [-1 for _ in range(n+1)]
self._paths = [-1 for _ in range(n + 1)]

for path in cities:
a, b = path[0], path[1]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{
'title': 'Sample Test case 1',
'k': 4,
'arr': [1, 2, 3, 4, 10, 20, 30, 40, 100, 200],
'arr': [1, 2, 3, 4, 10, 20, 30, 40, 100, 200],
'answer': 3
},
{
Expand Down
Loading