├── library-flit
├── README.md
├── demolib
│ ├── code.py
│ └── __init__.py
├── tests
│ └── test_code.py
├── pyproject.toml
└── azure-pipelines.yml
├── library-basic
├── demolib
│ ├── __init__.py
│ └── code.py
├── requirements.txt
├── tests
│ └── test_code.py
├── setup.py
└── azure-pipelines.yml
├── django-basic
├── azuredemo
│ ├── demo
│ │ ├── __init__.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── urls.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── azuredemo
│ │ ├── __init__.py
│ │ ├── wsgi.py
│ │ ├── urls.py
│ │ └── settings.py
│ └── manage.py
├── requirements.txt
├── pytest.ini
└── azure-pipelines.yml
├── django-multi-environment
├── azuredemo
│ ├── demo
│ │ ├── __init__.py
│ │ ├── migrations
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── urls.py
│ │ ├── tests.py
│ │ └── views.py
│ ├── azuredemo
│ │ ├── __init__.py
│ │ ├── wsgi.py
│ │ ├── urls.py
│ │ └── settings.py
│ └── manage.py
├── pytest.ini
└── azure-pipelines.yml
├── flask-basic
├── MANIFEST.in
├── setup.cfg
├── tests
│ ├── test_factory.py
│ ├── data.sql
│ ├── test_db.py
│ ├── conftest.py
│ ├── test_auth.py
│ └── test_blog.py
├── flaskr
│ ├── templates
│ │ ├── auth
│ │ │ ├── login.html
│ │ │ └── register.html
│ │ ├── blog
│ │ │ ├── create.html
│ │ │ ├── update.html
│ │ │ └── index.html
│ │ └── base.html
│ ├── schema.sql
│ ├── db.py
│ ├── __init__.py
│ ├── static
│ │ └── style.css
│ ├── auth.py
│ └── blog.py
├── setup.py
├── LICENSE
├── README.rst
└── azure-pipelines.yml
├── azure-pipelines.yml
├── README.md
├── django-basic.yml
├── LICENSE
├── .gitignore
└── basic-unittest.md
/library-flit/README.md:
--------------------------------------------------------------------------------
1 | Hello!
--------------------------------------------------------------------------------
/library-basic/demolib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/azuredemo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/azuredemo/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/django-basic/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==3.2.20
2 | pytz==2018.7
3 |
--------------------------------------------------------------------------------
/library-basic/demolib/code.py:
--------------------------------------------------------------------------------
1 | def example():
2 | return "banana!"
--------------------------------------------------------------------------------
/library-basic/requirements.txt:
--------------------------------------------------------------------------------
1 | pytest
2 | wheel
3 | # other things you might need?
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/django-basic/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | python_files = tests.py test_*.py *_tests.py
3 | DJANGO_SETTINGS_MODULE = azuredemo.settings
4 |
--------------------------------------------------------------------------------
/library-flit/demolib/code.py:
--------------------------------------------------------------------------------
1 | """Example module."""
2 |
3 |
4 | def example():
5 | """Give a banana."""
6 | return "banana!"
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DemoConfig(AppConfig):
5 | name = 'demo'
6 |
--------------------------------------------------------------------------------
/django-multi-environment/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | python_files = tests.py test_*.py *_tests.py
3 | DJANGO_SETTINGS_MODULE = azuredemo.settings
4 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class DemoConfig(AppConfig):
5 | name = 'demo'
6 |
--------------------------------------------------------------------------------
/library-flit/demolib/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | Demo library for azure pipelines.
3 |
4 | This package does nothing.
5 | """
6 |
7 | __version__ = "0.0.1.dev"
--------------------------------------------------------------------------------
/flask-basic/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include flaskr/schema.sql
3 | graft flaskr/static
4 | graft flaskr/templates
5 | graft tests
6 | global-exclude *.pyc
7 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 |
4 | from . import views
5 |
6 | urlpatterns = [
7 | path('', views.index, name='index'),
8 | ]
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 |
4 | from . import views
5 |
6 | urlpatterns = [
7 | path('', views.index, name='index'),
8 | ]
--------------------------------------------------------------------------------
/flask-basic/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | license_file = LICENSE
3 |
4 | [bdist_wheel]
5 | universal = True
6 |
7 | [tool:pytest]
8 | testpaths = tests
9 |
10 | [coverage:run]
11 | branch = True
12 | source =
13 | flaskr
14 |
--------------------------------------------------------------------------------
/library-basic/tests/test_code.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from demolib import code
4 |
5 |
6 | class ExampleTestCase(TestCase):
7 | def test_example(self):
8 | ret = code.example()
9 | assert ret == "banana!"
10 |
--------------------------------------------------------------------------------
/library-flit/tests/test_code.py:
--------------------------------------------------------------------------------
1 | from unittest import TestCase
2 |
3 | from demolib import code
4 |
5 |
6 | class ExampleTestCase(TestCase):
7 | def test_example(self):
8 | ret = code.example()
9 | assert ret == "banana!"
10 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | - master
3 | name: $(Date:yyyyMMdd)$(Rev:.r)
4 |
5 | jobs:
6 | - template: 'django-basic/azure-pipelines.yml'
7 | - template: 'django-multi-environment/azure-pipelines.yml'
8 | - template: 'flask-basic/azure-pipelines.yml'
9 | - template: 'library-basic/azure-pipelines.yml'
10 | - template: 'library-flit/azure-pipelines.yml'
11 |
--------------------------------------------------------------------------------
/flask-basic/tests/test_factory.py:
--------------------------------------------------------------------------------
1 | from flaskr import create_app
2 |
3 |
4 | def test_config():
5 | """Test create_app without passing test config."""
6 | assert not create_app().testing
7 | assert create_app({'TESTING': True}).testing
8 |
9 |
10 | def test_hello(client):
11 | response = client.get('/hello')
12 | assert response.data == b'Hello, World!'
13 |
--------------------------------------------------------------------------------
/flask-basic/tests/data.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO user (username, password)
2 | VALUES
3 | ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'),
4 | ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79');
5 |
6 | INSERT INTO post (title, body, author_id, created)
7 | VALUES
8 | ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00');
9 |
--------------------------------------------------------------------------------
/library-flit/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "flit",
4 | ]
5 | build-backend = "flit.buildapi"
6 |
7 | [tool.flit.metadata]
8 | module = "demolib"
9 | author = "Anthony Shaw"
10 | author-email = "anthonyshaw@apache.org"
11 | home-page = "https://github.com/tonybaloney/azure-pipelines-python-examples"
12 | classifiers = ["License :: OSI Approved :: MIT License"]
13 | requires = []
14 | requires-python = ">=3.6"
15 | description-file = "README.md"
16 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/azuredemo/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for azuredemo project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'azuredemo.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/azuredemo/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for azuredemo project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'azuredemo.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/templates/auth/login.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block header %}
4 |
{% block title %}Log In{% endblock %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/templates/auth/register.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block header %}
4 | {% block title %}Register{% endblock %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/templates/blog/create.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block header %}
4 | {% block title %}New Post{% endblock %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/schema.sql:
--------------------------------------------------------------------------------
1 | -- Initialize the database.
2 | -- Drop any existing data and create empty tables.
3 |
4 | DROP TABLE IF EXISTS user;
5 | DROP TABLE IF EXISTS post;
6 |
7 | CREATE TABLE user (
8 | id INTEGER PRIMARY KEY AUTOINCREMENT,
9 | username TEXT UNIQUE NOT NULL,
10 | password TEXT NOT NULL
11 | );
12 |
13 | CREATE TABLE post (
14 | id INTEGER PRIMARY KEY AUTOINCREMENT,
15 | author_id INTEGER NOT NULL,
16 | created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
17 | title TEXT NOT NULL,
18 | body TEXT NOT NULL,
19 | FOREIGN KEY (author_id) REFERENCES user (id)
20 | );
21 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, Client
2 |
3 | class DemoTestCase(TestCase):
4 | def test_index_no_user(self):
5 | c = Client()
6 | response = c.post('/demo/', {'person': None})
7 | assert response.status_code == 404
8 |
9 | def test_index_ground_control(self):
10 | c = Client()
11 | response = c.post('/demo/', {'person': 'ground_control', 'message': 'Ground Control to Major Tom'})
12 | assert response.status_code == 200
13 | assert 'the stars look very different today' in response.content.decode('utf-8')
14 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase, Client
2 |
3 | class DemoTestCase(TestCase):
4 | def test_index_no_user(self):
5 | c = Client()
6 | response = c.post('/demo/', {'person': None})
7 | assert response.status_code == 404
8 |
9 | def test_index_ground_control(self):
10 | c = Client()
11 | response = c.post('/demo/', {'person': 'ground_control', 'message': 'Ground Control to Major Tom'})
12 | assert response.status_code == 200
13 | assert 'the stars look very different today' in response.content.decode('utf-8')
14 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == '__main__':
6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'azuredemo.settings')
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == '__main__':
6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'azuredemo.settings')
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/demo/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.http import HttpResponse, Http404
3 |
4 | # Create your views here.
5 | def index(request):
6 | if 'person' in request.POST:
7 | if request.POST['person'] == 'ground_control' \
8 | and request.POST['message'] == 'Ground Control to Major Tom':
9 | return HttpResponse(
10 | """This is Major Tom to Ground Control,
11 | I'm stepping through the door,
12 | And I'm floating in a most peculiar way,
13 | And the stars look very different today.
""")
14 | raise Http404("Huh?")
15 |
--------------------------------------------------------------------------------
/library-basic/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 | from setuptools import find_packages, setup
4 |
5 |
6 | setup(
7 | name='demolib',
8 | version='1.0.0',
9 | url='http://flask.pocoo.org/docs/tutorial/',
10 | license='MIT',
11 | maintainer='Anthony Shaw',
12 | maintainer_email='anthonyshaw@apache.org',
13 | description='The basic app for testing.',
14 | long_description="",
15 | packages=find_packages(),
16 | include_package_data=True,
17 | zip_safe=False,
18 | install_requires=[
19 | ],
20 | extras_require={
21 | 'test': [
22 | 'pytest',
23 | 'coverage',
24 | ],
25 | },
26 | )
27 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/demo/views.py:
--------------------------------------------------------------------------------
1 | from django.shortcuts import render
2 | from django.http import HttpResponse, Http404
3 |
4 | # Create your views here.
5 | def index(request):
6 | if 'person' in request.POST:
7 | if request.POST['person'] == 'ground_control' \
8 | and request.POST['message'] == 'Ground Control to Major Tom':
9 | return HttpResponse(
10 | """This is Major Tom to Ground Control,
11 | I'm stepping through the door,
12 | And I'm floating in a most peculiar way,
13 | And the stars look very different today.
""")
14 | raise Http404("Huh?")
15 |
--------------------------------------------------------------------------------
/flask-basic/tests/test_db.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | import pytest
4 | from flaskr.db import get_db
5 |
6 |
7 | def test_get_close_db(app):
8 | with app.app_context():
9 | db = get_db()
10 | assert db is get_db()
11 |
12 | with pytest.raises(sqlite3.ProgrammingError) as e:
13 | db.execute('SELECT 1')
14 |
15 | assert 'closed' in str(e)
16 |
17 |
18 | def test_init_db_command(runner, monkeypatch):
19 | class Recorder(object):
20 | called = False
21 |
22 | def fake_init_db():
23 | Recorder.called = True
24 |
25 | monkeypatch.setattr('flaskr.db.init_db', fake_init_db)
26 | result = runner.invoke(args=['init-db'])
27 | assert 'Initialized' in result.output
28 | assert Recorder.called
29 |
--------------------------------------------------------------------------------
/flask-basic/setup.py:
--------------------------------------------------------------------------------
1 | import io
2 |
3 | from setuptools import find_packages, setup
4 |
5 | with io.open('README.rst', 'rt', encoding='utf8') as f:
6 | readme = f.read()
7 |
8 | setup(
9 | name='flaskr',
10 | version='1.0.0',
11 | url='http://flask.pocoo.org/docs/tutorial/',
12 | license='BSD',
13 | maintainer='Pallets team',
14 | maintainer_email='contact@palletsprojects.com',
15 | description='The basic blog app built in the Flask tutorial.',
16 | long_description=readme,
17 | packages=find_packages(),
18 | include_package_data=True,
19 | zip_safe=False,
20 | install_requires=[
21 | 'flask',
22 | ],
23 | extras_require={
24 | 'test': [
25 | 'pytest',
26 | 'coverage',
27 | ],
28 | },
29 | )
30 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/templates/blog/update.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block header %}
4 | {% block title %}Edit "{{ post['title'] }}"{% endblock %}
5 | {% endblock %}
6 |
7 | {% block content %}
8 |
9 | Title
10 |
11 | Body
12 | {{ request.form['body'] or post['body'] }}
13 |
14 |
15 |
16 |
17 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 | {% block title %}{% endblock %} - Flaskr
3 |
4 |
5 |
6 |
7 | {% if g.user %}
8 | {{ g.user['username'] }}
9 | Log Out
10 | {% else %}
11 | Register
12 | Log In
13 | {% endif %}
14 |
15 |
16 |
17 |
18 | {% block header %}{% endblock %}
19 |
20 | {% for message in get_flashed_messages() %}
21 | {{ message }}
22 | {% endfor %}
23 | {% block content %}{% endblock %}
24 |
25 |
--------------------------------------------------------------------------------
/django-basic/azuredemo/azuredemo/urls.py:
--------------------------------------------------------------------------------
1 | """azuredemo URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path, include
18 |
19 | urlpatterns = [
20 | path('admin/', admin.site.urls),
21 | path('demo/', include('demo.urls'))
22 | ]
23 |
--------------------------------------------------------------------------------
/django-multi-environment/azuredemo/azuredemo/urls.py:
--------------------------------------------------------------------------------
1 | """azuredemo URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path, include
18 |
19 | urlpatterns = [
20 | path('admin/', admin.site.urls),
21 | path('demo/', include('demo.urls'))
22 | ]
23 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/templates/blog/index.html:
--------------------------------------------------------------------------------
1 | {% extends 'base.html' %}
2 |
3 | {% block header %}
4 | {% block title %}Posts{% endblock %}
5 | {% if g.user %}
6 | New
7 | {% endif %}
8 | {% endblock %}
9 |
10 | {% block content %}
11 | {% for post in posts %}
12 |
13 |
22 | {{ post['body'] }}
23 |
24 | {% if not loop.last %}
25 |
26 | {% endif %}
27 | {% endfor %}
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # azure-pipelines-python-examples
2 | Example configurations for Azure Build Pipelines for Python
3 |
4 | [](https://dev.azure.com/AnthonyShaw/azure-pipelines-python-examples/_build/latest?definitionId=2?branchName=master)
5 |
6 | * [A Django 2.1 project, tested on Python 3.6 and 3.7](django-basic)
7 | * [A Django 2.1 project, tested on Python 3.6 and 3.7, but against 2 versions of Django (2.1.3 and 2.1.4)](django-multi-environment)
8 | * [A Flask project, tested on Python 3.6 and Python 3.7](flask-basic)
9 | * [A Python library with a wheel and source distribution](library-basic)
10 | * [A Python library built with flit and pyproject.toml](library-flit)
11 |
12 | The full detail on these is on [my blog](https://medium.com/@anthonypjshaw/azure-pipelines-with-python-by-example-aa65f4070634)
13 |
--------------------------------------------------------------------------------
/django-basic.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | - master
3 |
4 | jobs:
5 |
6 | - job: 'django_basic'
7 | pool:
8 | vmImage: 'Ubuntu-16.04'
9 | strategy:
10 | matrix:
11 | Python36:
12 | python.version: '3.6'
13 | Python37:
14 | python.version: '3.7'
15 | maxParallel: 2
16 |
17 | steps:
18 | - task: UsePythonVersion@0
19 | inputs:
20 | versionSpec: '$(python.version)'
21 | architecture: 'x64'
22 |
23 | - script: |
24 | python -m pip install --upgrade pip
25 | pip install -r django-basic/requirements.txt
26 | displayName: 'Install dependencies'
27 | - script: |
28 | pip install pytest-django
29 | cd django-basic/azuredemo
30 | pytest --junitxml=../../reports/django-basic.xml
31 | displayName: 'Run tests'
32 | - task: PublishTestResults@2
33 | inputs:
34 | testResultsFiles: 'reports/django-basic.xml'
35 | testRunTitle: '$(Agent.OS) - $(Build.DefinitionName) - Python $(python.version)'
36 | condition: succeededOrFailed()
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Anthony Shaw
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/django-basic/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # To use this pipeline standalone, uncomment the `trigger`
2 | # and `name` stanzas. As-is, the pipeline is suitable for
3 | # importing as a template
4 | #
5 | # trigger:
6 | # - master
7 | # name: $(Date:yyyyMMdd)$(Rev:.r)
8 | jobs:
9 |
10 | - job: 'django_basic'
11 | pool:
12 | vmImage: 'Ubuntu-16.04'
13 | strategy:
14 | matrix:
15 | Python36:
16 | python.version: '3.6'
17 | Python37:
18 | python.version: '3.7'
19 | maxParallel: 2
20 |
21 | steps:
22 | - task: UsePythonVersion@0
23 | inputs:
24 | versionSpec: '$(python.version)'
25 | architecture: 'x64'
26 |
27 | - script: |
28 | python -m pip install --upgrade pip
29 | pip install -r django-basic/requirements.txt
30 | displayName: 'Install dependencies'
31 |
32 | - script: |
33 | pip install pytest-django
34 | cd django-basic/azuredemo
35 | pytest --junitxml=../../reports/django-basic.xml
36 | displayName: 'Run tests'
37 |
38 | - task: PublishTestResults@2
39 | inputs:
40 | testResultsFiles: 'reports/django-basic.xml'
41 | testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)'
42 | condition: succeededOrFailed()
43 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/db.py:
--------------------------------------------------------------------------------
1 | import sqlite3
2 |
3 | import click
4 | from flask import current_app, g
5 | from flask.cli import with_appcontext
6 |
7 |
8 | def get_db():
9 | """Connect to the application's configured database. The connection
10 | is unique for each request and will be reused if this is called
11 | again.
12 | """
13 | if 'db' not in g:
14 | g.db = sqlite3.connect(
15 | current_app.config['DATABASE'],
16 | detect_types=sqlite3.PARSE_DECLTYPES
17 | )
18 | g.db.row_factory = sqlite3.Row
19 |
20 | return g.db
21 |
22 |
23 | def close_db(e=None):
24 | """If this request connected to the database, close the
25 | connection.
26 | """
27 | db = g.pop('db', None)
28 |
29 | if db is not None:
30 | db.close()
31 |
32 |
33 | def init_db():
34 | """Clear existing data and create new tables."""
35 | db = get_db()
36 |
37 | with current_app.open_resource('schema.sql') as f:
38 | db.executescript(f.read().decode('utf8'))
39 |
40 |
41 | @click.command('init-db')
42 | @with_appcontext
43 | def init_db_command():
44 | """Clear existing data and create new tables."""
45 | init_db()
46 | click.echo('Initialized the database.')
47 |
48 |
49 | def init_app(app):
50 | """Register database functions with the Flask app. This is called by
51 | the application factory.
52 | """
53 | app.teardown_appcontext(close_db)
54 | app.cli.add_command(init_db_command)
55 |
--------------------------------------------------------------------------------
/django-multi-environment/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # To use this pipeline standalone, uncomment the `trigger`
2 | # and `name` stanzas. As-is, the pipeline is suitable for
3 | # importing as a template
4 | #
5 | # trigger:
6 | # - master
7 | # name: $(Date:yyyyMMdd)$(Rev:.r)
8 | jobs:
9 |
10 | - job: 'django_multi_environment'
11 | pool:
12 | vmImage: 'Ubuntu-16.04'
13 | strategy:
14 | matrix:
15 | Python36_Django213:
16 | python.version: '3.6'
17 | django.version: '2.1.3'
18 | Python37_Django213:
19 | python.version: '3.7'
20 | django.version: '2.1.3'
21 | Python37_Django214:
22 | python.version: '3.7'
23 | django.version: '2.1.4'
24 | maxParallel: 3
25 |
26 | steps:
27 | - task: UsePythonVersion@0
28 | inputs:
29 | versionSpec: '$(python.version)'
30 | architecture: 'x64'
31 |
32 | - script: |
33 | python -m pip install --upgrade pip
34 | pip install django==$(django.version)
35 | displayName: 'Install dependencies'
36 |
37 | - script: |
38 | pip install pytest-django
39 | cd django-multi-environment/azuredemo
40 | pytest --junitxml=../../reports/django-multi-environment.xml
41 | displayName: 'Run tests'
42 |
43 | - task: PublishTestResults@2
44 | inputs:
45 | testResultsFiles: 'reports/django-multi-environment.xml'
46 | testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version) - Django $(django.version)'
47 | condition: succeededOrFailed()
48 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from flask import Flask
4 |
5 |
6 | def create_app(test_config=None):
7 | """Create and configure an instance of the Flask application."""
8 | app = Flask(__name__, instance_relative_config=True)
9 | app.config.from_mapping(
10 | # a default secret that should be overridden by instance config
11 | SECRET_KEY='dev',
12 | # store the database in the instance folder
13 | DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
14 | )
15 |
16 | if test_config is None:
17 | # load the instance config, if it exists, when not testing
18 | app.config.from_pyfile('config.py', silent=True)
19 | else:
20 | # load the test config if passed in
21 | app.config.update(test_config)
22 |
23 | # ensure the instance folder exists
24 | try:
25 | os.makedirs(app.instance_path)
26 | except OSError:
27 | pass
28 |
29 | @app.route('/hello')
30 | def hello():
31 | return 'Hello, World!'
32 |
33 | # register the database commands
34 | from flaskr import db
35 | db.init_app(app)
36 |
37 | # apply the blueprints to the app
38 | from flaskr import auth, blog
39 | app.register_blueprint(auth.bp)
40 | app.register_blueprint(blog.bp)
41 |
42 | # make url_for('index') == url_for('blog.index')
43 | # in another app, you might define a separate main index here with
44 | # app.route, while giving the blog blueprint a url_prefix, but for
45 | # the tutorial the blog will be the main index
46 | app.add_url_rule('/', endpoint='index')
47 |
48 | return app
49 |
--------------------------------------------------------------------------------
/flask-basic/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2010 by the Pallets team.
2 |
3 | Some rights reserved.
4 |
5 | Redistribution and use in source and binary forms of the software as
6 | well as documentation, with or without modification, are permitted
7 | provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice,
10 | this list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright
13 | notice, this list of conditions and the following disclaimer in the
14 | documentation and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
21 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
22 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
23 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
26 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 | USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
30 | THIS SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF
31 | SUCH DAMAGE.
32 |
--------------------------------------------------------------------------------
/library-basic/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # To use this pipeline standalone, uncomment the `trigger`
2 | # and `name` stanzas. As-is, the pipeline is suitable for
3 | # importing as a template
4 | #
5 | # trigger:
6 | # - master
7 | # name: $(Date:yyyyMMdd)$(Rev:.r)
8 | jobs:
9 |
10 | - job: 'library_basic'
11 | pool:
12 | vmImage: 'Ubuntu-16.04'
13 | strategy:
14 | matrix:
15 | Python36:
16 | python.version: '3.6'
17 | Python37:
18 | python.version: '3.7'
19 | maxParallel: 2
20 |
21 | steps:
22 | - task: UsePythonVersion@0
23 | inputs:
24 | versionSpec: '$(python.version)'
25 | architecture: 'x64'
26 |
27 | - script: |
28 | python -m pip install --upgrade pip
29 | pip install -r library-basic/requirements.txt
30 | cd library-basic
31 | pip install -e .
32 | displayName: 'Install dependencies'
33 |
34 | - script: |
35 | cd library-basic
36 | pytest --junitxml=../reports/library-basic.xml
37 | displayName: 'Run tests'
38 |
39 | - task: PublishTestResults@2
40 | inputs:
41 | testResultsFiles: 'reports/library-basic.xml'
42 | testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)'
43 | condition: succeededOrFailed()
44 |
45 | - script: |
46 | cd library-basic
47 | python setup.py sdist bdist_wheel
48 | displayName: 'Run tests'
49 |
50 | - task: CopyFiles@2
51 | inputs:
52 | contents: library-basic/dist/**
53 | targetFolder: $(Build.ArtifactStagingDirectory)
54 |
55 | - task: PublishBuildArtifacts@1
56 | inputs:
57 | pathtoPublish: $(Build.ArtifactStagingDirectory)
58 | artifactName: Library_Basic_Wheel
59 |
--------------------------------------------------------------------------------
/flask-basic/README.rst:
--------------------------------------------------------------------------------
1 | Flaskr
2 | ======
3 |
4 | The basic blog app built in the Flask `tutorial`_.
5 |
6 | .. _tutorial: http://flask.pocoo.org/docs/tutorial/
7 |
8 |
9 | Install
10 | -------
11 |
12 | **Be sure to use the same version of the code as the version of the docs
13 | you're reading.** You probably want the latest tagged version, but the
14 | default Git version is the master branch. ::
15 |
16 | # clone the repository
17 | $ git clone https://github.com/pallets/flask
18 | $ cd flask
19 | # checkout the correct version
20 | $ git tag # shows the tagged versions
21 | $ git checkout latest-tag-found-above
22 | $ cd examples/tutorial
23 |
24 | Create a virtualenv and activate it::
25 |
26 | $ python3 -m venv venv
27 | $ . venv/bin/activate
28 |
29 | Or on Windows cmd::
30 |
31 | $ py -3 -m venv venv
32 | $ venv\Scripts\activate.bat
33 |
34 | Install Flaskr::
35 |
36 | $ pip install -e .
37 |
38 | Or if you are using the master branch, install Flask from source before
39 | installing Flaskr::
40 |
41 | $ pip install -e ../..
42 | $ pip install -e .
43 |
44 |
45 | Run
46 | ---
47 |
48 | ::
49 |
50 | $ export FLASK_APP=flaskr
51 | $ export FLASK_ENV=development
52 | $ flask init-db
53 | $ flask run
54 |
55 | Or on Windows cmd::
56 |
57 | > set FLASK_APP=flaskr
58 | > set FLASK_ENV=development
59 | > flask init-db
60 | > flask run
61 |
62 | Open http://127.0.0.1:5000 in a browser.
63 |
64 |
65 | Test
66 | ----
67 |
68 | ::
69 |
70 | $ pip install '.[test]'
71 | $ pytest
72 |
73 | Run with coverage report::
74 |
75 | $ coverage run -m pytest
76 | $ coverage report
77 | $ coverage html # open htmlcov/index.html in a browser
78 |
--------------------------------------------------------------------------------
/flask-basic/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tempfile
3 |
4 | import pytest
5 | from flaskr import create_app
6 | from flaskr.db import get_db, init_db
7 |
8 | # read in SQL for populating test data
9 | with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f:
10 | _data_sql = f.read().decode('utf8')
11 |
12 |
13 | @pytest.fixture
14 | def app():
15 | """Create and configure a new app instance for each test."""
16 | # create a temporary file to isolate the database for each test
17 | db_fd, db_path = tempfile.mkstemp()
18 | # create the app with common test config
19 | app = create_app({
20 | 'TESTING': True,
21 | 'DATABASE': db_path,
22 | })
23 |
24 | # create the database and load test data
25 | with app.app_context():
26 | init_db()
27 | get_db().executescript(_data_sql)
28 |
29 | yield app
30 |
31 | # close and remove the temporary database
32 | os.close(db_fd)
33 | os.unlink(db_path)
34 |
35 |
36 | @pytest.fixture
37 | def client(app):
38 | """A test client for the app."""
39 | return app.test_client()
40 |
41 |
42 | @pytest.fixture
43 | def runner(app):
44 | """A test runner for the app's Click commands."""
45 | return app.test_cli_runner()
46 |
47 |
48 | class AuthActions(object):
49 | def __init__(self, client):
50 | self._client = client
51 |
52 | def login(self, username='test', password='test'):
53 | return self._client.post(
54 | '/auth/login',
55 | data={'username': username, 'password': password}
56 | )
57 |
58 | def logout(self):
59 | return self._client.get('/auth/logout')
60 |
61 |
62 | @pytest.fixture
63 | def auth(client):
64 | return AuthActions(client)
65 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 | **/venv/
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/basic-unittest.md:
--------------------------------------------------------------------------------
1 | # Basic Python Windows + Linux example
2 |
3 | This example uses nose2 as the test runner with the junit XML plugin. This is passed to Azure pipelines so the test output is cleanly parsed.
4 |
5 | Also, this example assumes:
6 | - Python 3.6 and 3.7 (change Matrix for Linux + Windows to alter)
7 | - Your tests are in a directory called "tests" (change the nose2 line to alter)
8 |
9 | ```yaml
10 | trigger:
11 | - master
12 |
13 | jobs:
14 |
15 | - job: 'Test_Linux'
16 | pool:
17 | vmImage: 'Ubuntu-16.04'
18 | strategy:
19 | matrix:
20 | Python36:
21 | python.version: '3.6'
22 | Python37:
23 | python.version: '3.7'
24 | maxParallel: 2
25 |
26 | steps:
27 | - task: UsePythonVersion@0
28 | inputs:
29 | versionSpec: '$(python.version)'
30 | architecture: 'x64'
31 |
32 | - script: |
33 | python -m pip install --upgrade pip
34 | pip install -r requirements.txt
35 | displayName: 'Install dependencies'
36 |
37 | - script: |
38 | pip install nose2
39 | nose2 --plugin nose2.plugins.junitxml --junit-xml tests
40 | displayName: 'Run tests'
41 |
42 | - task: PublishTestResults@2
43 | inputs:
44 | testResultsFiles: '**/nose2-junit.xml'
45 | testRunTitle: 'Python $(python.version)'
46 | condition: succeededOrFailed()
47 |
48 | - job: 'Test_Windows'
49 | pool:
50 | vmImage: 'vs2017-win2016'
51 | strategy:
52 | matrix:
53 | Python36:
54 | python.version: '3.6'
55 | Python37:
56 | python.version: '3.7'
57 | maxParallel: 2
58 |
59 | steps:
60 | - task: UsePythonVersion@0
61 | inputs:
62 | versionSpec: '$(python.version)'
63 | architecture: 'x64'
64 |
65 | - script: |
66 | python -m pip install --upgrade pip
67 | pip install -r requirements.txt
68 | displayName: 'Install dependencies'
69 |
70 | - script: |
71 | pip install nose2
72 | nose2 --plugin nose2.plugins.junitxml --junit-xml tests
73 | displayName: 'Run tests'
74 |
75 | - task: PublishTestResults@2
76 | inputs:
77 | testResultsFiles: '**\nose2-junit.xml'
78 | testRunTitle: 'Python $(python.version)'
79 | condition: succeededOrFailed()
80 |
81 | ```
82 |
--------------------------------------------------------------------------------
/flask-basic/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # To use this pipeline standalone, uncomment the `trigger`
2 | # and `name` stanzas. As-is, the pipeline is suitable for
3 | # importing as a template
4 | #
5 | # trigger:
6 | # - master
7 | # name: $(Date:yyyyMMdd)$(Rev:.r)
8 | jobs:
9 |
10 | - job: 'flask_basic'
11 | pool:
12 | vmImage: 'Ubuntu-16.04'
13 | strategy:
14 | matrix:
15 | Python36:
16 | python.version: '3.6'
17 | Python37:
18 | python.version: '3.7'
19 | maxParallel: 2
20 |
21 | steps:
22 | - task: UsePythonVersion@0
23 | inputs:
24 | versionSpec: '$(python.version)'
25 | architecture: 'x64'
26 |
27 | - script: |
28 | python -m pip install --upgrade pip
29 | cd flask-basic
30 | pip install '.[test]'
31 | displayName: 'Install dependencies'
32 |
33 | - script: |
34 | pip install pytest
35 | cd flask-basic
36 | pytest --junitxml=../reports/flask-basic.xml
37 | displayName: 'Run tests'
38 |
39 | - task: PublishTestResults@2
40 | inputs:
41 | testResultsFiles: 'reports/flask-basic.xml'
42 | testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)'
43 | condition: succeededOrFailed()
44 |
45 | - job: 'flask_basic_with_coverage'
46 | pool:
47 | vmImage: 'Ubuntu-16.04'
48 | strategy:
49 | matrix:
50 | Python36:
51 | python.version: '3.6'
52 | Python37:
53 | python.version: '3.7'
54 | maxParallel: 2
55 |
56 | steps:
57 | - task: UsePythonVersion@0
58 | inputs:
59 | versionSpec: '$(python.version)'
60 | architecture: 'x64'
61 |
62 | - script: |
63 | python -m pip install --upgrade pip
64 | cd flask-basic
65 | pip install '.[test]'
66 | displayName: 'Install dependencies'
67 |
68 | - script: |
69 | pip install pytest coverage
70 | cd flask-basic
71 | coverage run -m pytest --junitxml=../reports/flask-basic-coverage.xml
72 | coverage report
73 | displayName: 'Run tests and coverage'
74 |
75 | - task: PublishTestResults@2
76 | inputs:
77 | testResultsFiles: 'reports/flask-basic-coverage.xml'
78 | testRunTitle: '$(Agent.OS) - $(Build.BuildNumber)[$(Agent.JobName)] - Python $(python.version)'
79 | condition: succeededOrFailed()
80 |
--------------------------------------------------------------------------------
/flask-basic/tests/test_auth.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from flask import g, session
3 | from flaskr.db import get_db
4 |
5 |
6 | def test_register(client, app):
7 | # test that viewing the page renders without template errors
8 | assert client.get('/auth/register').status_code == 200
9 |
10 | # test that successful registration redirects to the login page
11 | response = client.post(
12 | '/auth/register', data={'username': 'a', 'password': 'a'}
13 | )
14 | assert 'http://localhost/auth/login' == response.headers['Location']
15 |
16 | # test that the user was inserted into the database
17 | with app.app_context():
18 | assert get_db().execute(
19 | "select * from user where username = 'a'",
20 | ).fetchone() is not None
21 |
22 |
23 | @pytest.mark.parametrize(('username', 'password', 'message'), (
24 | ('', '', b'Username is required.'),
25 | ('a', '', b'Password is required.'),
26 | ('test', 'test', b'already registered'),
27 | ))
28 | def test_register_validate_input(client, username, password, message):
29 | response = client.post(
30 | '/auth/register',
31 | data={'username': username, 'password': password}
32 | )
33 | assert message in response.data
34 |
35 |
36 | def test_login(client, auth):
37 | # test that viewing the page renders without template errors
38 | assert client.get('/auth/login').status_code == 200
39 |
40 | # test that successful login redirects to the index page
41 | response = auth.login()
42 | assert response.headers['Location'] == 'http://localhost/'
43 |
44 | # login request set the user_id in the session
45 | # check that the user is loaded from the session
46 | with client:
47 | client.get('/')
48 | assert session['user_id'] == 1
49 | assert g.user['username'] == 'test'
50 |
51 |
52 | @pytest.mark.parametrize(('username', 'password', 'message'), (
53 | ('a', 'test', b'Incorrect username.'),
54 | ('test', 'a', b'Incorrect password.'),
55 | ))
56 | def test_login_validate_input(auth, username, password, message):
57 | response = auth.login(username, password)
58 | assert message in response.data
59 |
60 |
61 | def test_logout(client, auth):
62 | auth.login()
63 |
64 | with client:
65 | auth.logout()
66 | assert 'user_id' not in session
67 |
--------------------------------------------------------------------------------
/flask-basic/flaskr/static/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-family: sans-serif;
3 | background: #eee;
4 | padding: 1rem;
5 | }
6 |
7 | body {
8 | max-width: 960px;
9 | margin: 0 auto;
10 | background: white;
11 | }
12 |
13 | h1, h2, h3, h4, h5, h6 {
14 | font-family: serif;
15 | color: #377ba8;
16 | margin: 1rem 0;
17 | }
18 |
19 | a {
20 | color: #377ba8;
21 | }
22 |
23 | hr {
24 | border: none;
25 | border-top: 1px solid lightgray;
26 | }
27 |
28 | nav {
29 | background: lightgray;
30 | display: flex;
31 | align-items: center;
32 | padding: 0 0.5rem;
33 | }
34 |
35 | nav h1 {
36 | flex: auto;
37 | margin: 0;
38 | }
39 |
40 | nav h1 a {
41 | text-decoration: none;
42 | padding: 0.25rem 0.5rem;
43 | }
44 |
45 | nav ul {
46 | display: flex;
47 | list-style: none;
48 | margin: 0;
49 | padding: 0;
50 | }
51 |
52 | nav ul li a, nav ul li span, header .action {
53 | display: block;
54 | padding: 0.5rem;
55 | }
56 |
57 | .content {
58 | padding: 0 1rem 1rem;
59 | }
60 |
61 | .content > header {
62 | border-bottom: 1px solid lightgray;
63 | display: flex;
64 | align-items: flex-end;
65 | }
66 |
67 | .content > header h1 {
68 | flex: auto;
69 | margin: 1rem 0 0.25rem 0;
70 | }
71 |
72 | .flash {
73 | margin: 1em 0;
74 | padding: 1em;
75 | background: #cae6f6;
76 | border: 1px solid #377ba8;
77 | }
78 |
79 | .post > header {
80 | display: flex;
81 | align-items: flex-end;
82 | font-size: 0.85em;
83 | }
84 |
85 | .post > header > div:first-of-type {
86 | flex: auto;
87 | }
88 |
89 | .post > header h1 {
90 | font-size: 1.5em;
91 | margin-bottom: 0;
92 | }
93 |
94 | .post .about {
95 | color: slategray;
96 | font-style: italic;
97 | }
98 |
99 | .post .body {
100 | white-space: pre-line;
101 | }
102 |
103 | .content:last-child {
104 | margin-bottom: 0;
105 | }
106 |
107 | .content form {
108 | margin: 1em 0;
109 | display: flex;
110 | flex-direction: column;
111 | }
112 |
113 | .content label {
114 | font-weight: bold;
115 | margin-bottom: 0.5em;
116 | }
117 |
118 | .content input, .content textarea {
119 | margin-bottom: 1em;
120 | }
121 |
122 | .content textarea {
123 | min-height: 12em;
124 | resize: vertical;
125 | }
126 |
127 | input.danger {
128 | color: #cc2f2e;
129 | }
130 |
131 | input[type=submit] {
132 | align-self: start;
133 | min-width: 10em;
134 | }
135 |
--------------------------------------------------------------------------------
/library-flit/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # To use this pipeline standalone, uncomment the `trigger`
2 | # and `name` stanzas. As-is, the pipeline is suitable for
3 | # importing as a template
4 | #
5 | # trigger:
6 | # - master
7 | # name: $(Date:yyyyMMdd)$(Rev:.r)
8 | jobs:
9 |
10 | - job: 'library_flit'
11 | pool:
12 | vmImage: 'Ubuntu-16.04'
13 | strategy:
14 | matrix:
15 | Python36:
16 | python.version: '3.6'
17 | Python37:
18 | python.version: '3.7'
19 | maxParallel: 2
20 |
21 | steps:
22 | - task: UsePythonVersion@0
23 | inputs:
24 | versionSpec: '$(python.version)'
25 | architecture: 'x64'
26 |
27 | - script: |
28 | python -m pip install --upgrade pip
29 | pip install pytest flit mock codecov pydocstyle pytest-cov
30 | cd library-flit
31 | flit install
32 | displayName: 'Install dependencies'
33 |
34 | - script: |
35 | cd library-flit
36 | python -m pytest tests/ --junitxml=../reports/library-flit.xml --cov=demolib
37 | codecov
38 | displayName: 'pytest'
39 | env:
40 | CODECOV_TOKEN: '/update', methods=('GET', 'POST'))
80 | @login_required
81 | def update(id):
82 | """Update a post if the current user is the author."""
83 | post = get_post(id)
84 |
85 | if request.method == 'POST':
86 | title = request.form['title']
87 | body = request.form['body']
88 | error = None
89 |
90 | if not title:
91 | error = 'Title is required.'
92 |
93 | if error is not None:
94 | flash(error)
95 | else:
96 | db = get_db()
97 | db.execute(
98 | 'UPDATE post SET title = ?, body = ? WHERE id = ?',
99 | (title, body, id)
100 | )
101 | db.commit()
102 | return redirect(url_for('blog.index'))
103 |
104 | return render_template('blog/update.html', post=post)
105 |
106 |
107 | @bp.route('//delete', methods=('POST',))
108 | @login_required
109 | def delete(id):
110 | """Delete a post.
111 |
112 | Ensures that the post exists and that the logged in user is the
113 | author of the post.
114 | """
115 | get_post(id)
116 | db = get_db()
117 | db.execute('DELETE FROM post WHERE id = ?', (id,))
118 | db.commit()
119 | return redirect(url_for('blog.index'))
120 |
--------------------------------------------------------------------------------