Skip to content

Commit

Permalink
Merge pull request #4 from VoronM1522/lab3
Browse files Browse the repository at this point in the history
Lab3
  • Loading branch information
VoronM1522 authored Feb 5, 2025
2 parents a58d252 + 3a00f78 commit 5169f78
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 13 deletions.
75 changes: 75 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Application CI

on:
push:

env:
USERNAME: voronm1522
DOCKER_IMAGE: voronm1522/devops
DOCKER_TAG: python-app

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Cache python
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ubuntu-python-${{ hashFiles('app_python/requirements.txt') }}
restore-keys: |
ubuntu-python-
- uses: actions/checkout@v3

- name: Set up python
uses: actions/setup-python@v3
with:
python-version: '3.10'

- name: Install dependencies and linter
run: |
python -m pip install --upgrade pip
pip install flake8
pip install -r app_python/requirements.txt
- name: Lint with flake8
run: |
flake8 app_python/app.py
- name: Run tests
run: |
python app_python/unittests.py
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ env.USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ubuntu-docker-${{ hashFiles('app_python/Dockerfile') }}
restore-keys: |
ubuntu-docker-
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: app_python
push: true
tags: ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }}

- name: Install snyk
run: npm install -g snyk

- name: Run snyk
run: |
cd app_python
snyk test
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

3 changes: 2 additions & 1 deletion app_python/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.md
Dockerfile
.dockerignore
.dockerignore
unittests.py
69 changes: 69 additions & 0 deletions app_python/.github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Application CI

on:
push:

env:
USERNAME: voronm1522
DOCKER_IMAGE: voronm1522/devops
DOCKER_TAG: python-app

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Cache python
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ubuntu-python-${{ hashFiles('app_python/requirements.txt') }}
restore-keys: |
ubuntu-python-
- uses: actions/checkout@v3

- name: Set up python
uses: actions/setup-python@v3
with:
python-version: '3.10'

- name: Install dependencies and linter
run: |
python -m pip install --upgrade pip
pip install flake8
pip install -r app_python/requirements.txt
- name: Lint with flake8
run: |
flake8 app_python/app.py
- name: Run tests
run: |
python app_python/unittests.py
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ env.USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}

- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ubuntu-docker-${{ hashFiles('app_python/Dockerfile') }}
restore-keys: |
ubuntu-docker-
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: app_python
push: true
tags: ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_TAG }}

- name: Run snyk
uses: snyk/actions/python-3.10@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
9 changes: 9 additions & 0 deletions app_python/CI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## CI

For optimizing CI I used the next best practices:
- Caching
- For python requirements
- For Dockerfile
- Vulnerability checking
- Atomic execution for commands (in one `run:` section)

22 changes: 22 additions & 0 deletions app_python/PYTHON.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,25 @@ The small size of the application does not allow for many practices. I have ende
## Coding standards, testing, code quality

To check compliance with code writing standards and evaluate its quality, pep8 (and pycodestyle) was used. At this stage, no automation of thesiting was performed. Everything is checked manually by sending additional requests (page refresh) and comparing the results. There were also tests with editing configuration file to ensure in correct configuration work.


## Unit Tests

There are 7 unit tests for application:
- Response code and template test;
- Test reading config file with:
- Valid time zone;
- Invalis timezone;
- Empty timezone;
- No configuration file test;
- Test time format in response;
- Test full response

I used next best practices:
- Naming:
- Each unit test (function) has comprehensive name, which allows to understand test's goal without description
- AAA pattern (Arrange, Act and Assert)
- Single component testing
- One move
- Fast tests
- No "magic strings"
22 changes: 22 additions & 0 deletions app_python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,25 @@ To run image run:
```
docker run <name>
```

## Unit Tests

There are unit tests for application in `unittests.py` file. To run it use:
```
python unittests.py
```
Output will contain `OK` in case of success or backtrace for function(s) with error.

## CI
[![Application CI](https://github.com/VoronM1522/S25-core-course-labs/actions/workflows/python-app.yml/badge.svg)](https://github.com/VoronM1522/S25-core-course-labs/actions/workflows/python-app.yml)

CI triggers on push. It runs on ubuntu-latest. It clones (copy) repository using `actions/checkout@v3` and execute the next steps:
- Setting up python;
- Dependencies and linter (`flake8`) installation;
- Linting the app;
- Running tests (unit tests);
- Login to Docker Hub:
- I use username as a plain text (environment variable), since it is not a secret from the previous pushes and README.md, where I describe pulling it from Docker Hub public repository. However password is a sencetive data, so I use GitHub Secrets
- Build & Push Docker image

As you can see, most of the steps use predefined workflaws.
Binary file added app_python/__pycache__/app.cpython-313.pyc
Binary file not shown.
19 changes: 7 additions & 12 deletions app_python/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,39 +9,34 @@
CONFIG_FILE = "config.txt"


def get_timezone() -> str():
def get_timezone(file=CONFIG_FILE) -> pytz.timezone:
"""
Loading timezone frome configuration file
"""
try:
with open(CONFIG_FILE, "r") as config_file:
with open(file, "r") as config_file:
time_zone = config_file.read().strip()

return time_zone if time_zone else "Europe/Moscow"
return pytz.timezone(time_zone)
except Exception:

# Default value
return "Europe/Moscow"
return pytz.timezone("Europe/Moscow")


@app.route("/")
def current_time() -> str():
def current_time() -> str:
"""
Returns the current time in timezone formatted as HH:MM:SS.
"""
global TIME_ZONE

# Set up TIME_ZONE if it was not
if TIME_ZONE == "":
time_zone = get_timezone()

try:
TIME_ZONE = pytz.timezone(time_zone)
except Exception:
TIME_ZONE = pytz.timezone("Europe/Moscow")
TIME_ZONE = get_timezone()

time = datetime.now(TIME_ZONE)
return f"Current time in {TIME_ZONE} is {time.strftime("%H:%M:%S")}"
return f"Current time in {TIME_ZONE} is {time.strftime('%H:%M:%S')}"


if __name__ == "__main__":
Expand Down
104 changes: 104 additions & 0 deletions app_python/unittests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import unittest, pytz, os
from app import app, current_time, get_timezone
from datetime import datetime



CONFIG_FILE = "config.txt"



class TestFlaskApp(unittest.TestCase):
def setUp(self):
"""Client settings"""

self.app = app.test_client()
self.app.testing = True


def test_current_time_endpoint(self):
"""Correctness test for endpoint '/'"""

response = self.app.get("/")

self.assertEqual(response.status_code, 200)
response_text = response.data.decode("utf-8")
self.assertIn("Current time in", response_text)
self.assertIn("is", response_text)


def test_get_timezone_valid(self):
"""Valid time zone reading test"""

if os.path.exists("test_get_timezone_valid_config.txt"):
os.remove("test_get_timezone_valid_config.txt")

with open("test_get_timezone_valid_config.txt", "w+") as file:
file.write("Europe/London\n")

timezone = get_timezone("test_get_timezone_valid_config.txt")
os.remove("test_get_timezone_valid_config.txt")
self.assertEqual(timezone, pytz.timezone("Europe/London"))


def test_get_timezone_invalid(self):
"""Invalid time zone reading test"""

if os.path.exists("test_get_timezone_invalid_config.txt"):
os.remove("test_get_timezone_invalid_config.txt")

with open("test_get_timezone_invalid_config.txt", "w+") as file:
file.write("Invalid/Timezone\n")

timezone = get_timezone("test_get_timezone_invalid_config.txt")
os.remove("test_get_timezone_invalid_config.txt")
self.assertEqual(timezone, pytz.timezone("Europe/Moscow"))


def test_get_timezone_empty(self):
"""Empty configure file testing"""

if os.path.exists("test_get_timezone_empty_config.txt"):
os.remove("test_get_timezone_empty_config.txt")

with open("test_get_timezone_empty_config.txt", "w+") as file:
file.write("\n")

timezone = get_timezone("test_get_timezone_empty_config.txt")
os.remove("test_get_timezone_empty_config.txt")
self.assertEqual(timezone, pytz.timezone("Europe/Moscow"))


def test_get_timezone_file_not_found(self):
"""No configuration file testing"""

timezone = get_timezone("")
self.assertEqual(timezone, pytz.timezone("Europe/Moscow"))


def test_timezone_format(self):
"""Format testing in response"""

response = self.app.get("/")
response_text = response.data.decode("utf-8")

import re
time_pattern = r"\d{2}:\d{2}:\d{2}"
self.assertTrue(re.search(time_pattern, response_text))


def test_full_response(self):
"""Full answer testing"""

try:
with open(CONFIG_FILE, "r") as config_file:
time_zone = pytz.timezone(config_file.read().strip())
except Exception:
time_zone = pytz.timezone("Europe/Moscow")

self.assertEqual(current_time(), f"Current time in {time_zone} is {datetime.now(time_zone).strftime('%H:%M:%S')}")



if __name__ == "__main__":
unittest.main()

0 comments on commit 5169f78

Please sign in to comment.