Skip to content
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
36 changes: 34 additions & 2 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ name: Python application
on:
push:
branches: [ "master" ]
paths: [ "app_python/**" ]
pull_request:
branches: [ "master" ]
paths: [ "app_python/**" ]
pull_request:
branches: [ "master" ]

Expand All @@ -14,25 +18,53 @@ permissions:

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

- name: Snyk
uses: snyk/actions/python-3.10@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: app_python/ --skip-unresolved

- name: Test
run: |
cd app_python
python -m unittest tests.py

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v6.13.0
with:
context: "{{defaultContext}}:app_python"
push: true
tags: ${{ vars.DOCKERHUB_USERNAME }}/moscow_time:latest
cache-from: type=gha
cache-to: type=gha,mode=max
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
Expand Down
14 changes: 14 additions & 0 deletions app_python/CI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

### Best Practices

1. __Path-Based Triggers__ - The workflow is executed only when any file within app_python/ directory was changed

2. __Password and Username Secrets__ - For security enhancement the `Secrets` variables allow hiding personal access tokens

3. __Variables__ - Make script more maintainable by reducing duplicated code framgents

4. __Explicit Version__ - The same as with `Dockerfile`, explicit tool versioning allows better code debugging and understanding its limits.

5. __Docker Caching__ - `cache-from` and `cache-to` attributes of building and pushing Docker image accelerate the process

6. __Snyk vulnerabilities check__ - Another Security enhancement
12 changes: 10 additions & 2 deletions app_python/PYTHON.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## Moscow Time Application
### 1. Choice Justification
Flask is easy and beginner-friendly framework that allows fast webapp coding. Hopefully, if the future labs will require us to extend this application, __flask lightweight nature__ will support highly covering the lack of __built-in admin interface__ that [Django](https://en.wikipedia.org/wiki/Django_(web_framework)) has.

Expand All @@ -7,5 +8,12 @@ Flask is easy and beginner-friendly framework that allows fast webapp coding. Ho
3. Confident requirements.txt via automatic __'pip freeze > requirements.txt'__ command.
4. Coding conventions: [Pylint](https://marketplace.visualstudio.com/items?itemName=ms-python.pylint) extension highlights problematic pieces of code to be changed.

### 3. Testing Implementation
So far the webapp is not so complex for requiring testing automatization ~~waiting for the next labs making to do this~~ .
## Unit-Testing
### 1. Unit-test list
* ```test_status_code(self)``` - makes sure that GET request successfully returns an html page with __200__ Status Code
* ```test_response_content(self)``` - Checks that the received webpage contains lines that we needed
* ```test_validate_time(self)``` - Time comparison between then one the program calculated and the one obtained from the response, with request-response delay considerations
### 2. Best Practices
* __Isolated environment__. Tests do not have any side effects hurting other tests' output
* __Setting test_app framework in class__. This avoids multiple application initialization for all the tests.
* __Assertion with considering possible environmental requests__. That is, when comparing the time values in assert, an error value must be introduced to allow some deviation between values to be approximately equal.
28 changes: 24 additions & 4 deletions app_python/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## Python Flask Moscow timezone Watch

![Python CI](https://github.com/IlsiyaNasibullina/S25-core-course-labs/actions/workflows/app_python.yml/badge.svg)

## Repository preparation

```bash
Expand Down Expand Up @@ -51,13 +53,31 @@ Result of running is as follows:

![](images/image_docker2.png)

## Running unittest

# ENDING
While being in the repository's directory, input the following commands:

To run distroless:
```bash
cd app_python
python -m unittest tests.py
```
This runs a few commands to check that the webapp works properly. In case of successful passing, the result should be similar to this:

```bash
docker build -f distroless.dockerfile -t dless .
docker run dless
...
----------------------------------------------------------------------
Ran 3 tests in 0.004s

OK
```

## CI Workflow

The workflow is configured to automate the following stages:

* Dependencies - using requerements.txt
* Lint - Checking coding conventions (line length, etc.)
* Snyk - Checks for security vulnerabilities.
* Test - tests.py unittest is called to verify functionality
* Docker - Builds and pushes the Docker image to Docker Hub.

70 changes: 70 additions & 0 deletions app_python/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'''
COMMENT
'''
from datetime import datetime
import re
import unittest
from moscow_app import app, MOSCOW


class TestMoscowApp(unittest.TestCase):
'''
DOCSTRING CLASS
'''
test_app = app.test_client()

def test_status_code(self):
'''
Check that request to html page is granted
'''
response = self.test_app.get('/')
self.assertEqual(response.status_code, 200)

def test_response_content(self):
'''
To check that the html content consists of required message
'''
response = self.test_app.get('/')
self.assertIn(b'Moscow Time:', response.data)

def test_validate_time(self):
'''
comment
'''
response = self.test_app.get('/')
msc = datetime.now(MOSCOW)
current = datetime.today()
# needed to measure time units to cast to a single integer
# meaning number of seconds
values = [3600, 60, 1]
# needed to prove approximate equality between
# 23:59:59 and 00:00:01 time dates, for instance
cycle = 3600 * 60 * 24
# error means what difference between times is allowed to be true
error = 5

response_text = response.data.decode('utf-8')

start = response_text.find("Moscow Time:")
msc_response = re.search(r'\d\d:\d\d:\d\d', response_text[start:]).group(0)
tmp = [int(x) for x in msc_response.split(":")]
msc_response = sum(a*b for a,b in zip(values,tmp))

start = response_text.find("Your Timezone:")
current_response = re.search(r'\d\d:\d\d:\d\d', response_text[start:]).group(0)
tmp = [int(x) for x in current_response.split(":")]
current_response = sum(a*b for a,b in zip(values, tmp))

# Gathering datetime the same way as in our app
tmp = [int(x) for x in msc.strftime("%H:%M:%S").split(":")]
msc = sum(a*b for a,b in zip(values, tmp))
tmp = [int(x) for x in current.strftime("%H:%M:%S").split(":")]
current = sum(a*b for a,b in zip(values, tmp))

self.assertTrue(abs(msc_response-msc) <= error
or abs(abs(msc_response - msc) - cycle) <= error)
self.assertTrue(abs(current_response-current) <= error
or abs(abs(current_response - current) - cycle) <= error)

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