diff --git a/.gitignore b/.gitignore index 3d76c77..8de69c4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ venv*/ # VS Code IDE /.vscode/ -*.code-workspace \ No newline at end of file +<<<<<<< HEAD +*.code-workspace diff --git a/README.md b/README.md index d0a76f3..2e32b07 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,18 @@ # File server project Author is Vasyl Pidhirskyi. + +## Сделанные требования + +Get port and working directory via arguments +Work independently without WSGI +Suit with RESTful API requirements +Provide sharing access to files and access policy +Protect files by cryptography tools +Use asynchronous programming concept +Work with database +Use multithreading for downloading files + +## Желаемые требования + +TODO: заполните требования diff --git a/demo/docstrings/demo.py b/demo/docstrings/demo.py new file mode 100644 index 0000000..361eb71 --- /dev/null +++ b/demo/docstrings/demo.py @@ -0,0 +1,31 @@ +"""This is demo module. + +It contains a lof useful functions. +Here you can find a class `MyClass`. +""" + + +class MyClass: + @staticmethod + def sum(a: int, b: int) -> int: + """Sum the values of two numbers. + + Args: + a (int): number 1 + b (int): number 2 + + Returns: + This is a description of what is returned. + + Raises: + KeyError: Raises an exception. + """ + return a + b + + +print(dir(MyClass.sum)) +print("__annotations__:", MyClass.sum.__annotations__) +print("__doc__:", MyClass.sum.__doc__) +print("call:", MyClass.sum(1, 2)) +print("call:", MyClass.sum("bug", "bug")) +print("call:", MyClass.sum(1, "bug")) diff --git a/demo/docstrings/docstring.md b/demo/docstrings/docstring.md new file mode 100644 index 0000000..4b448de --- /dev/null +++ b/demo/docstrings/docstring.md @@ -0,0 +1,16 @@ + +# Documentation + +https://www.python.org/dev/peps/pep-0257/ +https://docs.python-guide.org/writing/documentation/ + + +# Different styles + +https://stackoverflow.com/questions/3898572/what-is-the-standard-python-docstring-format + + +# + +- VS Code extension njpwerner.autodocstring +- PyCharm settings diff --git a/demo/exceptions/demo1.py b/demo/exceptions/demo1.py new file mode 100644 index 0000000..b76e338 --- /dev/null +++ b/demo/exceptions/demo1.py @@ -0,0 +1,11 @@ +def summa(n1, n2): + if type(n1) != 'int': + # TODO: show stack trace + raise TypeError('incorrect type: {}'.format(type(n1))) + return n1 - n2 + + +try: + summa('a', 'b') +except (TypeError, RuntimeError) as e: + print("error occurred {}".format(e.message)) diff --git a/demo/exceptions/exc_add_message.py b/demo/exceptions/exc_add_message.py new file mode 100644 index 0000000..4bbcd4b --- /dev/null +++ b/demo/exceptions/exc_add_message.py @@ -0,0 +1,11 @@ +try: + with open(path, "r") as f: + return dict(name=f.name, + content=f.read(), + create_date=time.strftime('%d.%M.%Y %H:%M:%S', time.gmtime(os.path.getctime(path))), + edit_date=time.strftime('%d.%M.%Y %H:%M:%S', time.gmtime(os.path.getmtime(path))), + size=int(os.path.getsize(path))) +except RuntimeError as err: + e.args += ('Custom message',) + ... + diff --git a/demo/exceptions/exception_main.py b/demo/exceptions/exception_main.py new file mode 100644 index 0000000..ec9ca2c --- /dev/null +++ b/demo/exceptions/exception_main.py @@ -0,0 +1,6 @@ +if __name__ == '__main__': + try: + main() + except BaseException as err: + # logging + print("всё плохо {}".format(err)) diff --git a/demo/exceptions/try-except.py b/demo/exceptions/try-except.py new file mode 100644 index 0000000..d0a9024 --- /dev/null +++ b/demo/exceptions/try-except.py @@ -0,0 +1,6 @@ +try: + d = int(input('>')) + x = 2 / d + print(x) +except Exception as e: + print(e) \ No newline at end of file diff --git a/demo/modules/example1/__init__.py b/demo/modules/example1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/modules/example1/mod1.py b/demo/modules/example1/mod1.py new file mode 100644 index 0000000..9393533 --- /dev/null +++ b/demo/modules/example1/mod1.py @@ -0,0 +1,11 @@ +# import mymodules.mod2 + +name = 'Me' + + +def whoami(): + print(name) + + +print("mod1: {}".format(__name__)) +started = True diff --git a/demo/modules/example1/modules.md b/demo/modules/example1/modules.md new file mode 100644 index 0000000..e69de29 diff --git a/demo/modules/example1/mymodules/__init__.py b/demo/modules/example1/mymodules/__init__.py new file mode 100644 index 0000000..93c9be9 --- /dev/null +++ b/demo/modules/example1/mymodules/__init__.py @@ -0,0 +1,3 @@ +import mod2 + +print('mymodules started') diff --git a/demo/modules/example1/mymodules/mod2.py b/demo/modules/example1/mymodules/mod2.py new file mode 100644 index 0000000..bd9413b --- /dev/null +++ b/demo/modules/example1/mymodules/mod2.py @@ -0,0 +1,5 @@ +def hello(): + print("Hello, world") + + +print("mod2: {}".format(__name__)) diff --git a/demo/modules/example2/changer.py b/demo/modules/example2/changer.py new file mode 100644 index 0000000..6565871 --- /dev/null +++ b/demo/modules/example2/changer.py @@ -0,0 +1,3 @@ +import config + +config.globalvar = '0' diff --git a/demo/modules/example2/config.py b/demo/modules/example2/config.py new file mode 100644 index 0000000..7790c68 --- /dev/null +++ b/demo/modules/example2/config.py @@ -0,0 +1,2 @@ + +globalvar = 'default' diff --git a/demo/modules/example2/main.py b/demo/modules/example2/main.py new file mode 100644 index 0000000..a143a46 --- /dev/null +++ b/demo/modules/example2/main.py @@ -0,0 +1,28 @@ +import config +import changer +import config as baseConfig +from config import globalvar as g # separate variable --> it's a copy, so changes don't affect it + +print('--- step 0 ---') +print(f'config.globalvar: {config.globalvar}') +print(f'baseConfig.globalvar: {baseConfig.globalvar}') +print(f'g: {g}') + +print('--- step 1 ---') +config.globalvar = '1' +print(f'config.globalvar: {config.globalvar}') +print(f'baseConfig.globalvar: {baseConfig.globalvar}') +print(f'g: {g}') + +print('--- step 2 ---') +baseConfig.globalvar = '2' +print(f'config.globalvar: {config.globalvar}') +print(f'baseConfig.globalvar: {baseConfig.globalvar}') +print(f'g: {g}') + + +print('--- step 3 ---') +g = '3' +print(f'config.globalvar: {config.globalvar}') +print(f'baseConfig.globalvar: {baseConfig.globalvar}') +print(f'g: {g}') diff --git a/demo/modules/example3/main.py b/demo/modules/example3/main.py new file mode 100644 index 0000000..442772c --- /dev/null +++ b/demo/modules/example3/main.py @@ -0,0 +1,5 @@ +import mypackage + +mypackage.init_function() +mypackage.mymodule.myfunction() +mypackage.f13() diff --git a/demo/modules/example3/mypackage/__init__.py b/demo/modules/example3/mypackage/__init__.py new file mode 100644 index 0000000..f6ee282 --- /dev/null +++ b/demo/modules/example3/mypackage/__init__.py @@ -0,0 +1,9 @@ + +# other good examples: +# https://github.com/pallets/flask/blob/main/src/flask/__init__.py + +from . import mymodule +from .mymodule import myfunction as f13 + +def init_function(): + return True diff --git a/demo/modules/example3/mypackage/mymodule.py b/demo/modules/example3/mypackage/mymodule.py new file mode 100644 index 0000000..cca9f43 --- /dev/null +++ b/demo/modules/example3/mypackage/mymodule.py @@ -0,0 +1,3 @@ + +def myfunction(): + return 13 diff --git a/demo/ospath/ospath.md b/demo/ospath/ospath.md new file mode 100644 index 0000000..4c33971 --- /dev/null +++ b/demo/ospath/ospath.md @@ -0,0 +1,65 @@ + +# Работа с файловой системой + +Широко используются два модуля `os` и `os.path`. + +## Модуль `os` + +[Документация `os`](https://docs.python.org/3/library/os.html) + +Модуль используется для работы с файлами на диске: + +os.chdir(path) - установить текущий каталог +os.getcwd() - получить текущий каталог +os.listdir(path='.') - получить список файлов в каталоге + +os.makedirs(name, mode=0o777, exist_ok=False) - создать папку (с вложенными папками) +os.mkdir(path, mode=0o777) - создать папку (родительские папки должны быть уже созданы) + +os.remove(path) - удалить файл +os.rmdir(path) - удалить пустую папку +os.removedirs(name) - удалить непустую папку + +Есть также функции по переименования, переносу файлов и другие. + +## Модуль `os.path` + +[Документация `os.path`](https://docs.python.org/3/library/os.path.html) + +Используется для работы с файловым путём: + +os.path.dirname(path) - получить имя родительской папки + +```python +>>> os.path.dirname('c:\\1\\2\\3.txt') +'c:\\1\\2' +``` + +os.path.join(path, *paths) - соединить несколько папок в один путь + +```python +>>> os.path.join(os.getcwd(), 'data') +'C:\\Work\\TC\\Trainings\\My\\python\\script-007\\demo\\argparsing\\data' + +>>> os.path.join(os.getcwd(), 'data', 'testfiles') +'C:\\Work\\TC\\Trainings\\My\\python\\script-007\\demo\\argparsing\\data\\testfiles' +``` + +os.path.lexists(path) - проверить, что файл или папка существует + +```python +>>> os.path.exists('c:\\1') +False + +>>> os.path.exists('c:\\') +True +``` + +os.path.is*(path) - проверить что объект является контретным типом + +os.path.islink(path) +os.path.isdir(path) +os.path.isfile(path) + + +os.path.getsize(path) - получить размер файла diff --git a/demo/tests/__init__.py b/demo/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/tests/myfuncs.py b/demo/tests/myfuncs.py new file mode 100644 index 0000000..47622f5 --- /dev/null +++ b/demo/tests/myfuncs.py @@ -0,0 +1,17 @@ +import os + + +def myadd(a, b): + if a > 100: + return 100 + return a + b + + +def tricky_func(): + a = 1 + b = a + 1 + return b + + +def checkfile(filename): + return os.path.isfile(filename) diff --git a/demo/tests/mypytests/.coveragerc b/demo/tests/mypytests/.coveragerc new file mode 100644 index 0000000..8a888f3 --- /dev/null +++ b/demo/tests/mypytests/.coveragerc @@ -0,0 +1,4 @@ +[run] +omit = tests/mypytests/* + tests/myunittests/* + tests/__init__.py diff --git a/demo/tests/mypytests/__init__.py b/demo/tests/mypytests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/tests/mypytests/conftest.py b/demo/tests/mypytests/conftest.py new file mode 100644 index 0000000..0c37597 --- /dev/null +++ b/demo/tests/mypytests/conftest.py @@ -0,0 +1,16 @@ +import os + +import pytest + + +@pytest.fixture(scope='function') +def prepare_testfile(request): + print('prepare_testfile: before test') + with open('testfile.txt', 'w') as f: + f.write('first line') + + yield # Run tests + + print('prepare_testfile: after test') + if os.path.exists('testfile.txt'): + os.remove('testfile.txt') diff --git a/demo/tests/mypytests/pytest.md b/demo/tests/mypytests/pytest.md new file mode 100644 index 0000000..b95fc95 --- /dev/null +++ b/demo/tests/mypytests/pytest.md @@ -0,0 +1,127 @@ + +# Documentation + +https://docs.pytest.org/en/latest/getting-started.html + +[Naming conventions](https://docs.pytest.org/en/6.2.x/reference.html#confval-python_classes): + +- files matching `test_*.py` and `*_test.py` will be considered test modules +- class names must start with `Test` and miss the `__init__` method +- pytest will consider any function prefixed with `test` as a test + +# Run tests + +Usage: + +```console +$ pytest [options] [file_or_dir] [file_or_dir] [...] +``` + +Success example: + +```console +$ pytest tests/mypytests +$ pytest tests/mypytests/test_0_myadd.py +``` + +# Capture output + +https://docs.pytest.org/en/6.2.x/capture.html + +Extra summary info can be shown using the '-r' option: + +```console +$ pytest --help | rg -e -r -C 3 + -r chars show extra test summary info as specified by chars: + (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, + (p)assed, (P)assed with output, (a)ll except passed + (p/P), or (A)ll. Warnings are displayed at all times +``` + +shows the captured output of passed tests: + +```console +$ pytest -rP +``` + +shows the captured output of failed tests (default behaviour): + +```console +$ pytest -rx +``` + +`-s` allows seeing output as is. + +# Coverage + +There is a plugin [pytest-cov](https://pypi.org/project/pytest-cov/) to measure code coverage: + +```console +$ pip install pytest-cov +``` + +Options are here: +- https://pytest-cov.readthedocs.io/en/latest/config.html#reference +- https://github.com/pytest-dev/pytest-cov/blob/master/src/pytest_cov/plugin.py + +Enable coverage: + +```console +$ pytest --cov tests tests\mypytests +============================= test session starts ============================= +platform win32 -- Python 2.7.18, pytest-4.6.11, py-1.10.0, pluggy-0.13.1 +rootdir: C:\Work\TC\Trainings\My\python\script-007\demo +plugins: cov-2.12.1 +collected 12 items + +tests\mypytests\test_0_myadd.py . [ 8%] +tests\mypytests\test_1_myadd_parametrize.py ... [ 33%] +tests\mypytests\test_2_before_after.py .... [ 66%] +tests\mypytests\test_3_conftest.py .. [ 83%] +tests\mypytests\test_4_class.py .. [100%] + +---------- coverage: platform win32, python 2.7.18-final-0 ----------- +Name Stmts Miss Cover +----------------------------------------------------------------- +tests\__init__.py 0 0 100% +tests\myfuncs.py 11 4 64% +tests\mypytests\__init__.py 0 0 100% +tests\mypytests\conftest.py 10 0 100% +tests\mypytests\test_0_myadd.py 6 0 100% +tests\mypytests\test_1_myadd_parametrize.py 4 0 100% +tests\mypytests\test_2_before_after.py 22 0 100% +tests\mypytests\test_3_conftest.py 6 0 100% +tests\mypytests\test_4_class.py 15 0 100% +tests\myunittests\__init__.py 0 0 100% +tests\myunittests\test_checkfile.py 14 14 0% +tests\myunittests\test_myadd.py 11 11 0% +----------------------------------------------------------------- +TOTAL 99 29 71% + + +========================== 12 passed in 0.32 seconds ========================== +``` + +Create HTML report: + +```console +$ pytest --cov tests --cov-report=html tests\mypytests +... +---------- coverage: platform win32, python 2.7.18-final-0 ----------- +Coverage HTML written to dir htmlcov +``` + +Omit some modules, files: + +```console +$ cat tests\mypytests\.coveragerc +[run] +omit = tests/mypytests/* + tests/myunittests/* + tests/__init__.py + +$ pytest --cov tests tests\mypytests --cov-config=tests\mypytests\.coveragerc +``` + +See other possibilities of `coverage` in [config file](https://coverage.readthedocs.io/en/latest/config.html#). + diff --git a/demo/tests/mypytests/requirements.txt b/demo/tests/mypytests/requirements.txt new file mode 100644 index 0000000..0c69b77 --- /dev/null +++ b/demo/tests/mypytests/requirements.txt @@ -0,0 +1,3 @@ +pytest +coverage +pytest-cov diff --git a/demo/tests/mypytests/test_0_myadd.py b/demo/tests/mypytests/test_0_myadd.py new file mode 100644 index 0000000..80b78e9 --- /dev/null +++ b/demo/tests/mypytests/test_0_myadd.py @@ -0,0 +1,15 @@ +from tests.myfuncs import myadd + + +def test_myadd(): + myadd_dataset = [ + 0, 0, 0, + 1, 2, 3, + 11, -5, 6, + # 11, 5, 6, # broken + ] + + for i in range(0, len(myadd_dataset), 3): + a, b, c = myadd_dataset[i:i + 3] + # assert is not a function! + assert c == myadd(a, b) diff --git a/demo/tests/mypytests/test_1_myadd_parametrize.py b/demo/tests/mypytests/test_1_myadd_parametrize.py new file mode 100644 index 0000000..27e8cc9 --- /dev/null +++ b/demo/tests/mypytests/test_1_myadd_parametrize.py @@ -0,0 +1,14 @@ +import pytest + +from tests.myfuncs import myadd + + +@pytest.mark.parametrize("num1, num2, sum", [ + (0, 0, 0), # test-case 1 + (1, 2, 3), # test-case 2 + (11, -5, 6), # test-case 3 + (11, 1, 12), # test-case 4 + # (11, 5, 6), # broken +]) +def test_myadd(num1, num2, sum): + assert myadd(num1, num2) == sum diff --git a/demo/tests/mypytests/test_1_myadd_parametrize2.py b/demo/tests/mypytests/test_1_myadd_parametrize2.py new file mode 100644 index 0000000..659a9ea --- /dev/null +++ b/demo/tests/mypytests/test_1_myadd_parametrize2.py @@ -0,0 +1,43 @@ +from http import HTTPStatus + +import pytest +import requests + +from config import BASE_API_URL + + +def test_login__correct_user__user_logged_in(test_user): + response = requests.post( + url=f"{BASE_API_URL}/login_web", + json={"username": test_user["username"], "password": test_user["password"]}, + ) + + assert response.status_code == HTTPStatus.OK + resp_data = response.json() + token_web = resp_data.pop("token_web", None) + assert token_web is not None, "No token_web in response" + assert resp_data == {"username": test_user["username"], "roles": []} + + +@pytest.mark.parametrize( + argnames=("test_username", "test_password"), + argvalues=( + ("test", ""), + ("test", "wrong_pass"), + ("wrong_user", "some_pass"), + ), + ids=( + "empty password", + "wrong password", + "wrong user", + ), +) +def test_login__incorrect_user__user_failed_to_login(test_username, test_password): + response = requests.post( + url=f"{BASE_API_URL}/login_web", + json={"username": test_username, "password": test_password}, + ) + + resp_data = response.json() + assert response.status_code == HTTPStatus.FORBIDDEN + assert resp_data == {"error": "Wrong username or password!"} diff --git a/demo/tests/mypytests/test_2_before_after.py b/demo/tests/mypytests/test_2_before_after.py new file mode 100644 index 0000000..50676a2 --- /dev/null +++ b/demo/tests/mypytests/test_2_before_after.py @@ -0,0 +1,42 @@ +import pytest + + +@pytest.fixture(scope='class', autouse=True) # Fixture, which executes before test suite implicitly +def suite_implicit(request): + print('suite_implicit: before test') + + +@pytest.fixture(scope='class') # Fixture, which executes before test suite explicitly +def suite_explicit(request): + print('suite_explicit: before test') + + +@pytest.fixture(scope='function', autouse=True) # Fixture, which executes before and after test case implicitly +def case_implicit(request): + print('case_implicit: before test-case') + yield # Run test case, transfer execution + print('case_implicit: after test-case') + + +@pytest.fixture(scope='function') # Fixture, which executes before and after test case explicitly +def case_explicit(request): + print('case_explicit: before test-case') + yield # Run test case, transfer execution + print('case_explicit: after test-case') + + +# Test suite +class TestSuite: + + # Test cases + def testcase3(self, case_explicit): + print('I like cookies') + + def testcase2(self, suite_explicit): + pass + + def testcase1(self, suite_explicit, case_explicit): # Add fixtures to test case + pass + + def testcase4(self): + pass diff --git a/demo/tests/mypytests/test_3_get_data.py b/demo/tests/mypytests/test_3_get_data.py new file mode 100644 index 0000000..d25f6dd --- /dev/null +++ b/demo/tests/mypytests/test_3_get_data.py @@ -0,0 +1,39 @@ +import pytest + +myvar = None + + +@pytest.fixture(scope='function', autouse=True) +def case_implicit_var(request): + print('case_implicit_var: before test-case') + global myvar + myvar = 'CASE_IMPLICIT_VAR' + yield myvar + print('case_implicit_var: after test-case') + + +@pytest.fixture(scope='function', autouse=True) +def case_implicit(request): + print('case_implicit: before test-case') + yield 'CASE_IMPLICIT' + print('case_implicit: after test-case') + + +@pytest.fixture(scope='function') +def case_explicit(request): + print('case_explicit: before test-case') + yield 'CASE_EXPLICIT' + print('case_explicit: after test-case') + + +class TestSuite: + + def testcase1(self): + print('testcase1: {}'.format(myvar)) + + def testcase2(self, case_explicit): + print('testcase2: {}'.format(case_explicit)) + + # if we need data from a fixture, then we can added it explicitly + def testcase3(self, case_implicit): + print('testcase3: {}'.format(case_implicit)) diff --git a/demo/tests/mypytests/test_4_get_data_class.py b/demo/tests/mypytests/test_4_get_data_class.py new file mode 100644 index 0000000..0e0f41b --- /dev/null +++ b/demo/tests/mypytests/test_4_get_data_class.py @@ -0,0 +1,25 @@ +import pytest + + +@pytest.fixture(scope='function') +def get_driver(request): + print('driver init') + driver = 'mydriver' # init + request.cls.driver = driver + yield + driver = None # deinit + + +class TestUserDriver1: + + @pytest.mark.usefixtures('get_driver') + def test_case1(self): + print('test_case1: {}'.format(self.driver)) + assert self.driver == 'mydriver' + + +class TestUserDriver2: + + def test_case2(self, get_driver): + print('test_case2: {}'.format(self.driver)) + assert self.driver == 'mydriver' diff --git a/demo/tests/mypytests/test_5_conftest.py b/demo/tests/mypytests/test_5_conftest.py new file mode 100644 index 0000000..d310f62 --- /dev/null +++ b/demo/tests/mypytests/test_5_conftest.py @@ -0,0 +1,10 @@ +from tests.myfuncs import checkfile + + +class TestCheckFile: + + def test_existent(self, prepare_testfile): + assert checkfile('testfile.txt') + + def test_nonexistent(self, prepare_testfile): + assert not checkfile('testfile123.txt') diff --git a/demo/tests/mypytests/test_6_exceptions.py b/demo/tests/mypytests/test_6_exceptions.py new file mode 100644 index 0000000..d39ad44 --- /dev/null +++ b/demo/tests/mypytests/test_6_exceptions.py @@ -0,0 +1,20 @@ +import os + +import pytest + +# More information about exception handling +# https://docs.pytest.org/en/6.2.x/assert.html#assertions-about-expected-exceptions + + +class TestExceptions: + + def test_exception(self): + with pytest.raises(AttributeError): + x = {'a': 1, 'b': 2} + print(x.a) + + def test_few_exceptions(self): + # on Windows and Linux + with pytest.raises((WindowsError, OSError)): + os.chdir('NotExistingDirectory') + diff --git a/main.py b/main.py new file mode 100644 index 0000000..a044654 --- /dev/null +++ b/main.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +from server import FileService +import argparse +import logging +import os + +PARENT_DIR = 'data' + +def main(): + logging.debug('Start server') + os.chdir(os.path.join(os.getcwd(), PARENT_DIR)) + aparser = argparse.ArgumentParser() + aparser.add_argument("-d", required=False, type=str, default=os.getcwd(), help="Change work directory") + + args = aparser.parse_args() + + if args.d: + FileService.change_dir(path=args.d) + + logging.debug('Stop server') + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG) + main() diff --git a/requirements.txt b/requirements.txt index e69de29..4342311 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,3 @@ +pytest +pytest-cov +coverage \ No newline at end of file diff --git a/server/FileService.py b/server/FileService.py new file mode 100644 index 0000000..35f4037 --- /dev/null +++ b/server/FileService.py @@ -0,0 +1,179 @@ +import os +import time +import logging + + +def get_path(filename: str) -> str: + path = os.path.join(os.getcwd(), filename) + if not is_valid_path(path): + raise ValueError(f"Invalid filename {path}") + + return path + + +def is_valid_path(path: str) -> bool: + forbidden = """# %&{}\<>*?/$!'":@+`|=""" + return bool(next((i for i in path if i not in forbidden), None)) + + +def is_allowed_path(path) -> bool: + return os.getcwd() in os.path.abspath(path) + + +def change_dir(path: str, autocreate: bool = True) -> None: + """Change current directory of app. + + Args: + path (str): Path to working directory with files. + autocreate (bool): Create folder if it doesn't exist. + + Raises: + RuntimeError: if directory does not exist and autocreate is False. + ValueError: if path is invalid. + """ + + if not is_valid_path(path): + raise ValueError(f"Invalid path {path}") + + if not is_allowed_path(path): + raise ValueError(f"Restricted path {path}") + + if not os.path.exists(path): + if autocreate: + os.makedirs(path) + else: + raise RuntimeError(f"Directory does not exist and autocreate is False.") + + os.chdir(path) + logging.debug(f"Set working directory to {os.getcwd()}") + + +def get_files() -> list: + """Get info about all files in working directory. + + Returns: + List of dicts, which contains info about each file. Keys: + - name (str): filename + - create_date (datetime): date of file creation. + - edit_date (datetime): date of last file modification. + - size (int): size of file in bytes. + """ + + folder = os.getcwd() + files = list() + for path in os.listdir(folder): + if os.path.isfile(path): + file_stat = { + 'name': os.path.basename(path), + 'create_date': time.ctime(os.path.getctime(path)), + 'edit_date': time.ctime(os.path.getmtime(path)), + 'size': os.path.getsize(path) + } + files.append(file_stat) + logging.debug(f"Get files from folder {folder}") + return files + + +def get_file_data(filename: str) -> dict: + """Get full info about file. + + Args: + filename (str): Filename. + + Returns: + Dict, which contains full info about file. Keys: + - name (str): filename + - content (str): file content + - create_date (datetime): date of file creation + - edit_date (datetime): date of last file modification + - size (int): size of file in bytes + + Raises: + RuntimeError: if file does not exist. + ValueError: if filename is invalid. + """ + + path = get_path(filename) + if not is_valid_path(path): + raise ValueError(f"Invalid filename {path}") + + if not is_allowed_path(path): + raise ValueError(f"Restricted path {path}") + + if not os.path.isfile(path) or not os.path.exists(path): + raise RuntimeError(f"RuntimeError: if file does not exist.") + + with open(path, 'r') as file: + logging.debug(f"Get files data {path}") + return { + 'name': os.path.basename(path), + 'create_date': time.ctime(os.path.getctime(path)), + 'edit_date': time.ctime(os.path.getmtime(path)), + 'content': file.read(), + 'size': os.path.getsize(path) + } + + +def create_file(filename: str, content: str = None) -> dict: + """Create a new file. + + Args: + filename (str): Filename. + content (str): String with file content. + + Returns: + Dict, which contains name of created file. Keys: + - name (str): filename + - content (str): file content + - create_date (datetime): date of file creation + - size (int): size of file in bytes + + Raises: + ValueError: if filename is invalid. + """ + + path = get_path(filename) + if not is_valid_path(path): + raise RuntimeError(f"RuntimeError: if file does not exist.") + + if not is_allowed_path(path): + raise ValueError(f"Restricted path {path}") + + if not os.path.exists(filename): + logging.warning(f"File already exist {path}") + + with open(filename, 'w') as file: + file.write(content) + + return { + 'name': os.path.basename(path), + 'create_date': time.ctime(os.path.getatime(path)), + 'content': content, + 'size': os.path.getsize(path) + } + + +def delete_file(filename: str) -> None: + """Delete file. + + Args: + filename (str): filename + + Raises: + RuntimeError: if file does not exist. + ValueError: if filename is invalid. + """ + + path = get_path(filename) + if not is_valid_path(path): + raise ValueError(f"Invalid filename {path}") + + if not is_allowed_path(path): + raise ValueError(f"Restricted path {path}") + + if not os.path.isfile(path) or not os.path.exists(path): + raise RuntimeError(f"RuntimeError: if file does not exist.") + + path = get_path(filename) + os.remove(os.path.join(path)) + logging.debug(f"File removed {path}") diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/__init__.py b/server/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/conftest.py b/server/tests/conftest.py new file mode 100644 index 0000000..ea9803c --- /dev/null +++ b/server/tests/conftest.py @@ -0,0 +1,33 @@ +import pytest +import os + + +@pytest.fixture(scope='function') +def set_default_folder(): + old_folder = os.getcwd() + os.chdir('.\\data') + yield + os.chdir(old_folder) + + +@pytest.fixture(scope='function') +def remove_file(): + yield + filepath = os.path.join(os.getcwd(), '0011.txt') + if os.path.isfile(filepath) and os.path.exists(filepath): + os.remove(filepath) + +@pytest.fixture(scope='function') +def remove_file(): + yield + filepath = os.path.join(os.getcwd(), '0011.txt') + if os.path.isfile(filepath) and os.path.exists(filepath): + os.remove(filepath) + + +@pytest.fixture(scope='function') +def create_file(): + filename = '0011.txt' + with open(filename, 'w') as file: + file.write('aaa') + yield diff --git a/server/tests/test_FileServer.py b/server/tests/test_FileServer.py new file mode 100644 index 0000000..dd36c44 --- /dev/null +++ b/server/tests/test_FileServer.py @@ -0,0 +1,80 @@ +import os +from server import FileService +import pytest + + +class TestChangeDir: + + def test_incorrect_input1(self, set_default_folder): + """Pass None as argument + """ + with pytest.raises(TypeError): + FileService.change_dir(None) + + def test_incorrect_input2(self, set_default_folder): + """Pass .\\1234 as argument + """ + + old_path = os.getcwd() + FileService.change_dir(".\\1234", autocreate=True) + assert old_path != os.getcwd() + + + def test_incorrect_input3(self, set_default_folder): + """Pass .\\1234\\1234 as argument + """ + + with pytest.raises(RuntimeError): + FileService.change_dir(".\\1234\\1234", autocreate=False) + + def test_incorrect_input4(self, set_default_folder): + """Pass 1235 as argument + """ + + with pytest.raises(RuntimeError): + FileService.change_dir("1235", autocreate=False) + + +class TestGetFiles: + + def test_incorrect_return1(self, set_default_folder): + """Pass . as argument + """ + + return isinstance(FileService.get_files(), list) + + +class TestGetFileData: + + def test_incorrect_input1(self, set_default_folder): + with pytest.raises(RuntimeError): + FileService.get_file_data("1234.txt") + + def test_incorrect_output(self, set_default_folder): + with pytest.raises(ValueError): + data = FileService.get_file_data('0011.png') + assert ('name', 'create_date', 'edit_date', 'content', 'size') == tuple(data.keys()) + + def test_incorrect_input2(self, set_default_folder): + with pytest.raises(RuntimeError): + FileService.get_file_data('001.png') + + +class TestCreateFile: + + def test_incorrect_output(self, set_default_folder, remove_file): + data = FileService.create_file('0011.txt', 'Hello World') + assert ('name', 'create_date', 'content', 'size') == tuple(data.keys()) + + def test_check_file(self, set_default_folder, remove_file): + filename = '0011.txt' + FileService.create_file(filename, 'Hello World') + assert os.path.isfile(os.path.join(os.getcwd(), filename)) + + +class TestDeleteFile: + + def test_delete_file(self, set_default_folder, create_file, remove_file): + filename = '0011.txt' + FileService.delete_file(filename) + assert not os.path.isfile(os.path.join(os.getcwd(), filename)) diff --git a/tasks/02_Add_file_service.md b/tasks/02_Add_file_service.md new file mode 100644 index 0000000..69bb3c1 --- /dev/null +++ b/tasks/02_Add_file_service.md @@ -0,0 +1,14 @@ + +# Обновление требований + +Согласно вашей концепции проекта добавьте реализуемые и желаемые требования в `README.md` + +# Реализация слоя работы с файлами и консольного приложения + +Для приложения необходимо реализовать: + +- Файл `/server/FileService.py` + - Все функции модуля `server.FileService`. Используйте библиотеки `os` и `os.path`. +- Файл `/main.py` + - Ключ командной строки `-d folder` для смены текущего каталога. +- Написать модульные тесты с использованием pytest \ No newline at end of file