Browse Source

Merge branch 'master' into 'development'

Master

See merge request warricksothr/Corvus!1
merge-requests/2/merge
Drew Short 5 years ago
parent
commit
8a33190da0
  1. 2
      README.md
  2. 13
      client-admin/.editorconfig
  3. 46
      client-admin/.gitignore
  4. 27
      client-admin/README.md
  5. 125
      client-admin/angular.json
  6. 12
      client-admin/browserslist
  7. 32
      client-admin/e2e/protractor.conf.js
  8. 23
      client-admin/e2e/src/app.e2e-spec.ts
  9. 11
      client-admin/e2e/src/app.po.ts
  10. 13
      client-admin/e2e/tsconfig.json
  11. 32
      client-admin/karma.conf.js
  12. 13032
      client-admin/package-lock.json
  13. 47
      client-admin/package.json
  14. 0
      client-admin/src/app/app.component.css
  15. 536
      client-admin/src/app/app.component.html
  16. 31
      client-admin/src/app/app.component.spec.ts
  17. 10
      client-admin/src/app/app.component.ts
  18. 16
      client-admin/src/app/app.module.ts
  19. 0
      client-admin/src/assets/.gitkeep
  20. 3
      client-admin/src/environments/environment.prod.ts
  21. 16
      client-admin/src/environments/environment.ts
  22. BIN
      client-admin/src/favicon.ico
  23. 13
      client-admin/src/index.html
  24. 12
      client-admin/src/main.ts
  25. 63
      client-admin/src/polyfills.ts
  26. 1
      client-admin/src/styles.css
  27. 20
      client-admin/src/test.ts
  28. 18
      client-admin/tsconfig.app.json
  29. 26
      client-admin/tsconfig.json
  30. 18
      client-admin/tsconfig.spec.json
  31. 91
      client-admin/tslint.json
  32. 7
      server/.gitignore
  33. 2
      server/Dockerfile
  34. 30
      server/Pipfile
  35. 714
      server/Pipfile.lock
  36. 3
      server/corvus/__init__.py
  37. 1
      server/corvus/api/__init__.py
  38. 95
      server/corvus/api/authentication_api.py
  39. 22
      server/corvus/api/health_api.py
  40. 15
      server/corvus/api/model.py
  41. 38
      server/corvus/api/user_api.py
  42. 3
      server/corvus/db.py
  43. 5
      server/corvus/errors.py
  44. 71
      server/corvus/middleware/authentication_middleware.py
  45. 4
      server/corvus/model/user_model.py
  46. 21
      server/corvus/service/authentication_service.py
  47. 23
      server/corvus/service/patch_service.py
  48. 39
      server/corvus/service/role_service.py
  49. 14
      server/corvus/service/transformation_service.py
  50. 52
      server/corvus/service/user_token_service.py
  51. 15
      server/corvus/service/validation_service.py
  52. 5
      server/dev-run.sh
  53. 31
      server/manage.py
  54. 8
      server/migrations/README
  55. 1
      server/mypy.ini
  56. 30
      server/run_tests.bat
  57. 30
      server/run_tests.sh
  58. 140
      server/tests/api/test_authentication_api.py
  59. 19
      server/tests/api/test_health_api.py
  60. 47
      server/tests/api/test_user_api.py
  61. 12
      server/tests/conftest.py
  62. 112
      server/tests/middleware/test_authentication_middleware.py
  63. 38
      server/tests/service/test_authentication_service.py
  64. 7
      server/tests/service/test_patch_service.py
  65. 6
      server/tests/service/test_role_service.py

2
README.md

@ -14,7 +14,7 @@ More information available at server/README.md
The administration SPA. The administration SPA.
More information available at administration/README.md
More information available at client-admin/README.md
## Release History ## Release History

13
client-admin/.editorconfig

@ -0,0 +1,13 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
client-admin/.gitignore

@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

27
client-admin/README.md

@ -0,0 +1,27 @@
# ClientAdmin
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.3.8.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

125
client-admin/angular.json

@ -0,0 +1,125 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"client-admin": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/client-admin",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "client-admin:build"
},
"configurations": {
"production": {
"browserTarget": "client-admin:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "client-admin:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "client-admin:serve"
},
"configurations": {
"production": {
"devServerTarget": "client-admin:serve:production"
}
}
}
}
}},
"defaultProject": "client-admin"
}

12
client-admin/browserslist

@ -0,0 +1,12 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

32
client-admin/e2e/protractor.conf.js

@ -0,0 +1,32 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

23
client-admin/e2e/src/app.e2e-spec.ts

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('client-admin app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

11
client-admin/e2e/src/app.po.ts

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo() {
return browser.get(browser.baseUrl) as Promise<any>;
}
getTitleText() {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

13
client-admin/e2e/tsconfig.json

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

32
client-admin/karma.conf.js

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/client-admin'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

13032
client-admin/package-lock.json
File diff suppressed because it is too large
View File

47
client-admin/package.json

@ -0,0 +1,47 @@
{
"name": "client-admin",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~8.2.9",
"@angular/common": "~8.2.9",
"@angular/compiler": "~8.2.9",
"@angular/core": "~8.2.9",
"@angular/forms": "~8.2.9",
"@angular/platform-browser": "~8.2.9",
"@angular/platform-browser-dynamic": "~8.2.9",
"@angular/router": "~8.2.9",
"rxjs": "~6.4.0",
"tslib": "^1.10.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.803.8",
"@angular/cli": "~8.3.8",
"@angular/compiler-cli": "~8.2.9",
"@angular/language-service": "~8.2.9",
"@types/node": "~8.9.4",
"@types/jasmine": "~3.3.8",
"@types/jasminewd2": "~2.0.3",
"codelyzer": "^5.0.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "~2.0.1",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.15.0",
"typescript": "~3.5.3"
}
}

0
client-admin/src/app/app.component.css

536
client-admin/src/app/app.component.html

@ -0,0 +1,536 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
color: #333;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 8px 0;
}
p {
margin: 0;
}
.spacer {
flex: 1;
}
.toolbar {
height: 60px;
margin: -8px;
display: flex;
align-items: center;
background-color: #1976d2;
color: white;
font-weight: 600;
}
.toolbar img {
margin: 0 16px;
}
.toolbar #twitter-logo {
height: 40px;
margin: 0 16px;
}
.toolbar #twitter-logo:hover {
opacity: 0.8;
}
.content {
display: flex;
margin: 32px auto;
padding: 0 16px;
max-width: 960px;
flex-direction: column;
align-items: center;
}
svg.material-icons {
height: 24px;
width: auto;
}
svg.material-icons:not(:last-child) {
margin-right: 8px;
}
.card svg.material-icons path {
fill: #888;
}
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 16px;
}
.card {
border-radius: 4px;
border: 1px solid #eee;
background-color: #fafafa;
height: 40px;
width: 200px;
margin: 0 8px 16px;
padding: 16px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
line-height: 24px;
}
.card-container .card:not(:last-child) {
margin-right: 0;
}
.card.card-small {
height: 16px;
width: 168px;
}
.card-container .card:not(.highlight-card) {
cursor: pointer;
}
.card-container .card:not(.highlight-card):hover {
transform: translateY(-3px);
box-shadow: 0 4px 17px rgba(black, 0.35);
}
.card-container .card:not(.highlight-card):hover .material-icons path {
fill: rgb(105, 103, 103);
}
.card.highlight-card {
background-color: #1976d2;
color: white;
font-weight: 600;
border: none;
width: auto;
min-width: 30%;
position: relative;
}
.card.card.highlight-card span {
margin-left: 60px;
}
svg#rocket {
width: 80px;
position: absolute;
left: -10px;
top: -24px;
}
svg#rocket-smoke {
height: 100vh;
position: absolute;
top: 10px;
right: 180px;
z-index: -10;
}
a,
a:visited,
a:hover {
color: #1976d2;
text-decoration: none;
}
a:hover {
color: #125699;
}
.terminal {
position: relative;
width: 80%;
max-width: 600px;
border-radius: 6px;
padding-top: 45px;
margin-top: 8px;
overflow: hidden;
background-color: rgb(15, 15, 16);
}
.terminal::before {
content: "\2022 \2022 \2022";
position: absolute;
top: 0;
left: 0;
height: 4px;
background: rgb(58, 58, 58);
color: #c2c3c4;
width: 100%;
font-size: 2rem;
line-height: 0;
padding: 14px 0;
text-indent: 4px;
}
.terminal pre {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
color: white;
padding: 0 1rem 1rem;
margin: 0;
}
.circle-link {
height: 40px;
width: 40px;
border-radius: 40px;
margin: 8px;
background-color: white;
border: 1px solid #eeeeee;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
transition: 1s ease-out;
}
.circle-link:hover {
transform: translateY(-0.25rem);
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
}
footer {
margin-top: 8px;
display: flex;
align-items: center;
line-height: 20px;
}
footer a {
display: flex;
align-items: center;
}
.github-star-badge {
color: #24292e;
display: flex;
align-items: center;
font-size: 12px;
padding: 3px 10px;
border: 1px solid rgba(27,31,35,.2);
border-radius: 3px;
background-image: linear-gradient(-180deg,#fafbfc,#eff3f6 90%);
margin-left: 4px;
font-weight: 600;
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;
}
.github-star-badge:hover {
background-image: linear-gradient(-180deg,#f0f3f6,#e6ebf1 90%);
border-color: rgba(27,31,35,.35);
background-position: -.5em;
}
.github-star-badge .material-icons {
height: 16px;
width: 16px;
margin-right: 4px;
}
svg#clouds {
position: fixed;
bottom: -160px;
left: -230px;
z-index: -10;
width: 1920px;
}
/* Responsive Styles */
@media screen and (max-width: 767px) {
.card-container > *:not(.circle-link) ,
.terminal {
width: 100%;
}
.card:not(.highlight-card) {
height: 16px;
margin: 8px 0;
}
.card.highlight-card span {
margin-left: 72px;
}
svg#rocket-smoke {
right: 120px;
transform: rotate(-5deg);
}
}
@media screen and (max-width: 575px) {
svg#rocket-smoke {
display: none;
visibility: hidden;
}
}
</style>
<!-- Toolbar -->
<div class="toolbar" role="banner">
<img
width="40"
alt="Angular Logo"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="
/>
<span>Welcome</span>
<div class="spacer"></div>
<a aria-label="Angular on twitter" target="_blank" rel="noopener" href="https://twitter.com/angular" title="Twitter">
<svg id="twitter-logo" height="24" data-name="Logo — FIXED" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
<defs>
<style>
.cls-1 {
fill: none;
}
.cls-2 {
fill: #ffffff;
}
</style>
</defs>
<rect class="cls-1" width="400" height="400" />
<path class="cls-2" d="M153.62,301.59c94.34,0,145.94-78.16,145.94-145.94,0-2.22,0-4.43-.15-6.63A104.36,104.36,0,0,0,325,122.47a102.38,102.38,0,0,1-29.46,8.07,51.47,51.47,0,0,0,22.55-28.37,102.79,102.79,0,0,1-32.57,12.45,51.34,51.34,0,0,0-87.41,46.78A145.62,145.62,0,0,1,92.4,107.81a51.33,51.33,0,0,0,15.88,68.47A50.91,50.91,0,0,1,85,169.86c0,.21,0,.43,0,.65a51.31,51.31,0,0,0,41.15,50.28,51.21,51.21,0,0,1-23.16.88,51.35,51.35,0,0,0,47.92,35.62,102.92,102.92,0,0,1-63.7,22A104.41,104.41,0,0,1,75,278.55a145.21,145.21,0,0,0,78.62,23"
/>
</svg>
</a>
</div>
<div class="content" role="main">
<!-- Highlight Card -->
<div class="card highlight-card card-small">
<svg id="rocket" alt="Rocket Ship" xmlns="http://www.w3.org/2000/svg" width="101.678" height="101.678" viewBox="0 0 101.678 101.678">
<g id="Group_83" data-name="Group 83" transform="translate(-141 -696)">
<circle id="Ellipse_8" data-name="Ellipse 8" cx="50.839" cy="50.839" r="50.839" transform="translate(141 696)" fill="#dd0031"/>
<g id="Group_47" data-name="Group 47" transform="translate(165.185 720.185)">
<path id="Path_33" data-name="Path 33" d="M3.4,42.615a3.084,3.084,0,0,0,3.553,3.553,21.419,21.419,0,0,0,12.215-6.107L9.511,30.4A21.419,21.419,0,0,0,3.4,42.615Z" transform="translate(0.371 3.363)" fill="#fff"/>
<path id="Path_34" data-name="Path 34" d="M53.3,3.221A3.09,3.09,0,0,0,50.081,0,48.227,48.227,0,0,0,18.322,13.437c-6-1.666-14.991-1.221-18.322,7.218A33.892,33.892,0,0,1,9.439,25.1l-.333.666a3.013,3.013,0,0,0,.555,3.553L23.985,43.641a2.9,2.9,0,0,0,3.553.555l.666-.333A33.892,33.892,0,0,1,32.647,53.3c8.55-3.664,8.884-12.326,7.218-18.322A48.227,48.227,0,0,0,53.3,3.221ZM34.424,9.772a6.439,6.439,0,1,1,9.106,9.106,6.368,6.368,0,0,1-9.106,0A6.467,6.467,0,0,1,34.424,9.772Z" transform="translate(0 0.005)" fill="#fff"/>
</g>
</g>
</svg>
<span>{{ title }} app is running!</span>
<svg id="rocket-smoke" alt="Rocket Ship Smoke" xmlns="http://www.w3.org/2000/svg" width="516.119" height="1083.632" viewBox="0 0 516.119 1083.632">
<path id="Path_40" data-name="Path 40" d="M644.6,141S143.02,215.537,147.049,870.207s342.774,201.755,342.774,201.755S404.659,847.213,388.815,762.2c-27.116-145.51-11.551-384.124,271.9-609.1C671.15,139.365,644.6,141,644.6,141Z" transform="translate(-147.025 -140.939)" fill="#f5f5f5"/>
</svg>
</div>
<!-- Resources -->
<h2>Resources</h2>
<p>Here are some links to help you get started:</p>
<div class="card-container">
<a class="card" target="_blank" rel="noopener" href="https://angular.io/tutorial">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3L1 9l11 6 9-4.91V17h2V9L12 3z"/></svg>
<span>Learn Angular</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg> </a>
<a class="card" target="_blank" rel="noopener" href="https://angular.io/cli">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
<span>CLI Documentation</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://blog.angular.io/">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/></svg>
<span>Angular Blog</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</a>
</div>
<!-- Next Steps -->
<h2>Next Steps</h2>
<p>What do you want to do next with your app?</p>
<input type="hidden" #selection>
<div class="card-container">
<div class="card card-small" (click)="selection.value = 'component'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>New Component</span>
</div>
<div class="card card-small" (click)="selection.value = 'material'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Angular Material</span>
</div>
<div class="card card-small" (click)="selection.value = 'dependency'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Add Dependency</span>
</div>
<div class="card card-small" (click)="selection.value = 'test'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Run and Watch Tests</span>
</div>
<div class="card card-small" (click)="selection.value = 'build'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Build for Production</span>
</div>
</div>
<!-- Terminal -->
<div class="terminal" [ngSwitch]="selection.value">
<pre *ngSwitchDefault>ng generate component xyz</pre>
<pre *ngSwitchCase="'material'">ng add @angular/material</pre>
<pre *ngSwitchCase="'dependency'">ng add _____</pre>
<pre *ngSwitchCase="'test'">ng test</pre>
<pre *ngSwitchCase="'build'">ng build --prod</pre>
</div>
<!-- Links -->
<div class="card-container">
<a class="circle-link" title="Animations" href="https://angular.io/guide/animations" target="_blank" rel="noopener">
<svg id="Group_20" data-name="Group 20" xmlns="http://www.w3.org/2000/svg" width="21.813" height="23.453" viewBox="0 0 21.813 23.453">
<path id="Path_15" data-name="Path 15" d="M4099.584,972.736h0l-10.882,3.9,1.637,14.4,9.245,5.153,9.245-5.153,1.686-14.4Z" transform="translate(-4088.702 -972.736)" fill="#ffa726"/>
<path id="Path_16" data-name="Path 16" d="M4181.516,972.736v23.453l9.245-5.153,1.686-14.4Z" transform="translate(-4170.633 -972.736)" fill="#fb8c00"/>
<path id="Path_17" data-name="Path 17" d="M4137.529,1076.127l-7.7-3.723,4.417-2.721,7.753,3.723Z" transform="translate(-4125.003 -1058.315)" fill="#ffe0b2"/>
<path id="Path_18" data-name="Path 18" d="M4137.529,1051.705l-7.7-3.723,4.417-2.721,7.753,3.723Z" transform="translate(-4125.003 -1036.757)" fill="#fff3e0"/>
<path id="Path_19" data-name="Path 19" d="M4137.529,1027.283l-7.7-3.723,4.417-2.721,7.753,3.723Z" transform="translate(-4125.003 -1015.199)" fill="#fff"/>
</svg>
</a>
<a class="circle-link" title="CLI" href="https://cli.angular.io/" target="_blank" rel="noopener">
<svg alt="Angular CLI Logo" xmlns="http://www.w3.org/2000/svg" width="21.762" height="23.447" viewBox="0 0 21.762 23.447">
<g id="Group_21" data-name="Group 21" transform="translate(0)">
<path id="Path_20" data-name="Path 20" d="M2660.313,313.618h0l-10.833,3.9,1.637,14.4,9.2,5.152,9.244-5.152,1.685-14.4Z" transform="translate(-2649.48 -313.618)" fill="#37474f"/>
<path id="Path_21" data-name="Path 21" d="M2741.883,313.618v23.447l9.244-5.152,1.685-14.4Z" transform="translate(-2731.05 -313.618)" fill="#263238"/>
<path id="Path_22" data-name="Path 22" d="M2692.293,379.169h11.724V368.618h-11.724Zm11.159-.6h-10.608v-9.345h10.621v9.345Z" transform="translate(-2687.274 -362.17)" fill="#fff"/>
<path id="Path_23" data-name="Path 23" d="M2709.331,393.688l.4.416,2.265-2.28-2.294-2.294-.4.4,1.893,1.893Z" transform="translate(-2702.289 -380.631)" fill="#fff"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="3.517" height="0.469" transform="translate(9.709 13.744)" fill="#fff"/>
</g>
</svg>
</a>
<a class="circle-link" title="Augury" href="https://augury.rangle.io/" target="_blank" rel="noopener">
<svg alt="Angular Augury Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="21.81" height="23.447" viewBox="0 0 21.81 23.447">
<defs>
<clipPath id="clip-path">
<rect id="Rectangle_13" data-name="Rectangle 13" width="10.338" height="10.27" fill="none"/>
</clipPath>
</defs>
<g id="Group_25" data-name="Group 25" transform="translate(0)">
<path id="Path_24" data-name="Path 24" d="M3780.155,311.417h0l-10.881,3.9,1.637,14.4,9.244,5.152,9.244-5.152,1.685-14.4Z" transform="translate(-3769.274 -311.417)" fill="#4a3493"/>
<path id="Path_25" data-name="Path 25" d="M3862.088,311.417v23.447l9.244-5.152,1.685-14.4Z" transform="translate(-3851.207 -311.417)" fill="#311b92"/>
<g id="Group_24" data-name="Group 24" transform="translate(6.194 6.73)" opacity="0.5">
<g id="Group_23" data-name="Group 23" transform="translate(0 0)">
<g id="Group_22" data-name="Group 22" clip-path="url(#clip-path)">
<path id="Path_26" data-name="Path 26" d="M3832.4,373.252a5.168,5.168,0,1,1-5.828-4.383,5.216,5.216,0,0,1,2.574.3,3.017,3.017,0,1,0,3.252,4.086Z" transform="translate(-3822.107 -368.821)" fill="#fff"/>
</g>
</g>
</g>
<path id="Path_27" data-name="Path 27" d="M3830.582,370.848a5.162,5.162,0,1,1-3.254-4.086,3.017,3.017,0,1,0,3.252,4.086Z" transform="translate(-3814.311 -359.969)" fill="#fff"/>
</g>
</svg>
</a>
<a class="circle-link" title="Protractor" href="https://www.protractortest.org/" target="_blank" rel="noopener">
<svg alt="Angular Protractor Logo" xmlns="http://www.w3.org/2000/svg" width="21.81" height="23.447" viewBox="0 0 21.81 23.447">
<g id="Group_26" data-name="Group 26" transform="translate(0)">
<path id="Path_28" data-name="Path 28" d="M4620.155,311.417h0l-10.881,3.9,1.637,14.4,9.244,5.152,9.244-5.152,1.685-14.4Z" transform="translate(-4609.274 -311.417)" fill="#e13439"/>
<path id="Path_29" data-name="Path 29" d="M4702.088,311.417v23.447l9.244-5.152,1.685-14.4Z" transform="translate(-4691.207 -311.417)" fill="#b52f32"/>
<path id="Path_30" data-name="Path 30" d="M4651.044,369.58v-.421h1.483a7.6,7.6,0,0,0-2.106-5.052l-1.123,1.123-.3-.3,1.122-1.121a7.588,7.588,0,0,0-4.946-2.055v1.482h-.421v-1.485a7.589,7.589,0,0,0-5.051,2.058l1.122,1.121-.3.3-1.123-1.123a7.591,7.591,0,0,0-2.106,5.052h1.482v.421h-1.489v1.734h15.241V369.58Zm-10.966-.263a4.835,4.835,0,0,1,9.67,0Z" transform="translate(-4634.008 -355.852)" fill="#fff"/>
</g>
</svg>
</a>
<a class="circle-link" title="Find a Local Meetup" href="https://www.meetup.com/find/?keywords=angular" target="_blank" rel="noopener">
<svg alt="Meetup Logo" xmlns="http://www.w3.org/2000/svg" width="24.607" height="23.447" viewBox="0 0 24.607 23.447">
<path id="logo--mSwarm" d="M21.221,14.95A4.393,4.393,0,0,1,17.6,19.281a4.452,4.452,0,0,1-.8.069c-.09,0-.125.035-.154.117a2.939,2.939,0,0,1-2.506,2.091,2.868,2.868,0,0,1-2.248-.624.168.168,0,0,0-.245-.005,3.926,3.926,0,0,1-2.589.741,4.015,4.015,0,0,1-3.7-3.347,2.7,2.7,0,0,1-.043-.38c0-.106-.042-.146-.143-.166a3.524,3.524,0,0,1-1.516-.69A3.623,3.623,0,0,1,2.23,14.557a3.66,3.66,0,0,1,1.077-3.085.138.138,0,0,0,.026-.2,3.348,3.348,0,0,1-.451-1.821,3.46,3.46,0,0,1,2.749-3.28.44.44,0,0,0,.355-.281,5.072,5.072,0,0,1,3.863-3,5.028,5.028,0,0,1,3.555.666.31.31,0,0,0,.271.03A4.5,4.5,0,0,1,18.3,4.7a4.4,4.4,0,0,1,1.334,2.751,3.658,3.658,0,0,1,.022.706.131.131,0,0,0,.1.157,2.432,2.432,0,0,1,1.574,1.645,2.464,2.464,0,0,1-.7,2.616c-.065.064-.051.1-.014.166A4.321,4.321,0,0,1,21.221,14.95ZM13.4,14.607a2.09,2.09,0,0,0,1.409,1.982,4.7,4.7,0,0,0,1.275.221,1.807,1.807,0,0,0,.9-.151.542.542,0,0,0,.321-.545.558.558,0,0,0-.359-.534,1.2,1.2,0,0,0-.254-.078c-.262-.047-.526-.086-.787-.138a.674.674,0,0,1-.617-.75,3.394,3.394,0,0,1,.218-1.109c.217-.658.509-1.286.79-1.918a15.609,15.609,0,0,0,.745-1.86,1.95,1.95,0,0,0,.06-1.073,1.286,1.286,0,0,0-1.051-1.033,1.977,1.977,0,0,0-1.521.2.339.339,0,0,1-.446-.042c-.1-.092-.2-.189-.307-.284a1.214,1.214,0,0,0-1.643-.061,7.563,7.563,0,0,1-.614.512A.588.588,0,0,1,10.883,8c-.215-.115-.437-.215-.659-.316a2.153,2.153,0,0,0-.695-.248A2.091,2.091,0,0,0,7.541,8.562a9.915,9.915,0,0,0-.405.986c-.559,1.545-1.015,3.123-1.487,4.7a1.528,1.528,0,0,0,.634,1.777,1.755,1.755,0,0,0,1.5.211,1.35,1.35,0,0,0,.824-.858c.543-1.281,1.032-2.584,1.55-3.875.142-.355.28-.712.432-1.064a.548.548,0,0,1,.851-.24.622.622,0,0,1,.185.539,2.161,2.161,0,0,1-.181.621c-.337.852-.68,1.7-1.018,2.552a2.564,2.564,0,0,0-.173.528.624.624,0,0,0,.333.71,1.073,1.073,0,0,0,.814.034,1.22,1.22,0,0,0,.657-.655q.758-1.488,1.511-2.978.35-.687.709-1.37a1.073,1.073,0,0,1,.357-.434.43.43,0,0,1,.463-.016.373.373,0,0,1,.153.387.7.7,0,0,1-.057.236c-.065.157-.127.316-.2.469-.42.883-.846,1.763-1.262,2.648A2.463,2.463,0,0,0,13.4,14.607Zm5.888,6.508a1.09,1.09,0,0,0-2.179.006,1.09,1.09,0,0,0,2.179-.006ZM1.028,12.139a1.038,1.038,0,1,0,.01-2.075,1.038,1.038,0,0,0-.01,2.075ZM13.782.528a1.027,1.027,0,1,0-.011,2.055A1.027,1.027,0,0,0,13.782.528ZM22.21,6.95a.882.882,0,0,0-1.763.011A.882.882,0,0,0,22.21,6.95ZM4.153,4.439a.785.785,0,1,0,.787-.78A.766.766,0,0,0,4.153,4.439Zm8.221,18.22a.676.676,0,1,0-.677.666A.671.671,0,0,0,12.374,22.658ZM22.872,12.2a.674.674,0,0,0-.665.665.656.656,0,0,0,.655.643.634.634,0,0,0,.655-.644A.654.654,0,0,0,22.872,12.2ZM7.171-.123A.546.546,0,0,0,6.613.43a.553.553,0,1,0,1.106,0A.539.539,0,0,0,7.171-.123ZM24.119,9.234a.507.507,0,0,0-.493.488.494.494,0,0,0,.494.494.48.48,0,0,0,.487-.483A.491.491,0,0,0,24.119,9.234Zm-19.454,9.7a.5.5,0,0,0-.488-.488.491.491,0,0,0-.487.5.483.483,0,0,0,.491.479A.49.49,0,0,0,4.665,18.936Z" transform="translate(0 0.123)" fill="#f64060"/>
</svg>
</a>
<a class="circle-link" title="Join the Conversation on Gitter" href="https://gitter.im/angular/angular" target="_blank" rel="noopener">
<svg alt="Gitter Logo" xmlns="http://www.w3.org/2000/svg" width="19.447" height="19.447" viewBox="0 0 19.447 19.447">
<g id="Group_40" data-name="Group 40" transform="translate(-1612 -405)">
<rect id="Rectangle_19" data-name="Rectangle 19" width="19.447" height="19.447" transform="translate(1612 405)" fill="#e60257"/>
<g id="gitter" transform="translate(1617.795 408.636)">
<g id="Group_33" data-name="Group 33" transform="translate(0 0)">
<rect id="Rectangle_15" data-name="Rectangle 15" width="1.04" height="9.601" transform="translate(2.304 2.324)" fill="#fff"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="1.04" height="9.601" transform="translate(4.607 2.324)" fill="#fff"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="1.04" height="4.648" transform="translate(6.91 2.324)" fill="#fff"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="1.04" height="6.971" transform="translate(0 0)" fill="#fff"/>
</g>
</g>
</g>
</svg>
</a>
</div>
<!-- Footer -->
<footer>
Love Angular?&nbsp;
<a href="https://github.com/angular/angular" target="_blank" rel="noopener"> Give our repo a star.
<div class="github-star-badge">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
Star
</div>
</a>
<a href="https://github.com/angular/angular" target="_blank" rel="noopener">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="#1976d2"/><path d="M0 0h24v24H0z" fill="none"/></svg>
</a>
</footer>
<svg id="clouds" alt="Gray Clouds Background" xmlns="http://www.w3.org/2000/svg" width="2611.084" height="485.677" viewBox="0 0 2611.084 485.677">
<path id="Path_39" data-name="Path 39" d="M2379.709,863.793c10-93-77-171-168-149-52-114-225-105-264,15-75,3-140,59-152,133-30,2.83-66.725,9.829-93.5,26.25-26.771-16.421-63.5-23.42-93.5-26.25-12-74-77-130-152-133-39-120-212-129-264-15-54.084-13.075-106.753,9.173-138.488,48.9-31.734-39.726-84.4-61.974-138.487-48.9-52-114-225-105-264,15a162.027,162.027,0,0,0-103.147,43.044c-30.633-45.365-87.1-72.091-145.206-58.044-52-114-225-105-264,15-75,3-140,59-152,133-53,5-127,23-130,83-2,42,35,72,70,86,49,20,106,18,157,5a165.625,165.625,0,0,0,120,0c47,94,178,113,251,33,61.112,8.015,113.854-5.72,150.492-29.764a165.62,165.62,0,0,0,110.861-3.236c47,94,178,113,251,33,31.385,4.116,60.563,2.495,86.487-3.311,25.924,5.806,55.1,7.427,86.488,3.311,73,80,204,61,251-33a165.625,165.625,0,0,0,120,0c51,13,108,15,157-5a147.188,147.188,0,0,0,33.5-18.694,147.217,147.217,0,0,0,33.5,18.694c49,20,106,18,157,5a165.625,165.625,0,0,0,120,0c47,94,178,113,251,33C2446.709,1093.793,2554.709,922.793,2379.709,863.793Z" transform="translate(142.69 -634.312)" fill="#eee"/>
</svg>
</div>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->

31
client-admin/src/app/app.component.spec.ts

@ -0,0 +1,31 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'client-admin'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('client-admin');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('client-admin app is running!');
});
});

10
client-admin/src/app/app.component.ts

@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'client-admin';
}

16
client-admin/src/app/app.module.ts

@ -0,0 +1,16 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

0
client-admin/src/assets/.gitkeep

3
client-admin/src/environments/environment.prod.ts

@ -0,0 +1,3 @@
export const environment = {
production: true
};

16
client-admin/src/environments/environment.ts

@ -0,0 +1,16 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

BIN
client-admin/src/favicon.ico

After

Width: 28  |  Height: 30  |  Size: 948 B

13
client-admin/src/index.html

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ClientAdmin</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

12
client-admin/src/main.ts

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

63
client-admin/src/polyfills.ts

@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags.ts';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

1
client-admin/src/styles.css

@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

20
client-admin/src/test.ts

@ -0,0 +1,20 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: any;
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

18
client-admin/tsconfig.app.json

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.ts"
],
"exclude": [
"src/test.ts",
"src/**/*.spec.ts"
]
}

26
client-admin/tsconfig.json

@ -0,0 +1,26 @@
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}

18
client-admin/tsconfig.spec.json

@ -0,0 +1,18 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine",
"node"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

91
client-admin/tslint.json

@ -0,0 +1,91 @@
{
"extends": "tslint:recommended",
"rules": {
"array-type": false,
"arrow-parens": false,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"import-blacklist": [
true,
"rxjs/Rx"
],
"interface-name": false,
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-access": false,
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-consecutive-blank-lines": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"object-literal-sort-keys": false,
"ordered-imports": false,
"quotemark": [
true,
"single"
],
"trailing-comma": false,
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}

7
server/.gitignore

@ -0,0 +1,7 @@
.idea/
.mypy_cache
.pytest_cache
.*_credentials
.coverage
*.iml

2
server/Dockerfile

@ -1,4 +1,4 @@
FROM python:3.6-slim-stretch
FROM python:3.7-slim-stretch
MAINTAINER Drew Short <warrick@sothr.com> MAINTAINER Drew Short <warrick@sothr.com>
ENV CORVUS_APP_DIRECTORY /opt/corvus ENV CORVUS_APP_DIRECTORY /opt/corvus

30
server/Pipfile

@ -4,27 +4,27 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
flask = ">=1.0,<1.1"
flask-sqlalchemy = ">=2.3,<2.4"
flask-migrate = ">=2.1,<2.2"
pynacl = ">=1.2,<1.3"
click = ">=6.7,<6.8"
rfc3339 = ">=6.0,<6.1"
flask = ">=1.1,<1.2"
flask-sqlalchemy = ">=2.4,<2.5"
flask-migrate = ">=2.5,<2.6"
python-dotenv = ">=0.10,<0.11"
pynacl = ">=1.3,<1.4"
click = ">=7.0,<7.1"
rfc3339 = ">=6.2,<6.3"
iso8601 = ">=0.1,<0.2" iso8601 = ">=0.1,<0.2"
[dev-packages] [dev-packages]
python-dotenv = ">=0.8,<0.9"
pytest = ">=3.6<3.7"
pytest = ">=5.2,<5.3"
coverage = ">=4.5,<4.6" coverage = ">=4.5,<4.6"
pycodestyle = ">=2.4,<2.5"
mypy = ">=0.620,<1.0"
mock = ">=2.0,<2.1"
pylint = ">=2.0,<2.1"
pydocstyle = ">=2.1,<2.2"
sphinx = ">=1.7,<1.8"
pycodestyle = ">=2.5,<2.6"
mypy = ">=0.730,<1.0"
mock = ">=3.0,<3.1"
pylint = ">=2.4,<2.5"
pydocstyle = ">=4.0,<4.1"
sphinx = ">=1.8,<1.9"
sphinx-rtd-theme = ">=0.4,<0.5" sphinx-rtd-theme = ">=0.4,<0.5"
sphinxcontrib-httpdomain = ">=1.7,<1.8" sphinxcontrib-httpdomain = ">=1.7,<1.8"
sphinx-jsondomain = "*" sphinx-jsondomain = "*"
[requires] [requires]
python_version = "3.6"
python_version = "3.7"

714
server/Pipfile.lock

@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "9ba0840844223ce8320a900a02e33f5ea9314301a827f49f0b92a4eff7227812"
"sha256": "79477dccc0014acea82818b6431dccece7c6ae8f6543c3f9ac1480283fc160ec"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.6"
"python_version": "3.7"
}, },
"sources": [ "sources": [
{ {
@ -18,79 +18,74 @@
"default": { "default": {
"alembic": { "alembic": {
"hashes": [ "hashes": [
"sha256:52d73b1d750f1414fa90c25a08da47b87de1e4ad883935718a8f36396e19e78e",
"sha256:eb7db9b4510562ec37c91d00b00d95fde076c1030d3f661aea882eec532b3565"
"sha256:9f907d7e8b286a1cfb22db9084f9ce4fde7ad7956bb496dc7c952e10ac90e36a"
], ],
"version": "==1.0.0"
"version": "==1.2.1"
}, },
"cffi": { "cffi": {
"hashes": [ "hashes": [
"sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743",
"sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef",
"sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50",
"sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f",
"sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30",
"sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93",
"sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257",
"sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b",
"sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3",
"sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e",
"sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc",
"sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04",
"sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6",
"sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359",
"sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596",
"sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b",
"sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd",
"sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95",
"sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5",
"sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e",
"sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6",
"sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca",
"sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31",
"sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1",
"sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2",
"sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085",
"sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801",
"sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4",
"sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184",
"sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917",
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"version": "==1.11.5"
"sha256:041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774",
"sha256:046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d",
"sha256:066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90",
"sha256:066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b",
"sha256:2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63",
"sha256:300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45",
"sha256:34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25",
"sha256:46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3",
"sha256:4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b",
"sha256:4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647",
"sha256:4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016",
"sha256:50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4",
"sha256:55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb",
"sha256:5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753",
"sha256:59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7",
"sha256:73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9",
"sha256:a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f",
"sha256:a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8",
"sha256:a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f",
"sha256:a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc",
"sha256:ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42",
"sha256:b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3",
"sha256:d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909",
"sha256:d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45",
"sha256:dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d",
"sha256:e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512",
"sha256:e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff",
"sha256:ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"
],
"version": "==1.12.3"
}, },
"click": { "click": {
"hashes": [ "hashes": [
"sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d",
"sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b"
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.7"
"version": "==7.0"
}, },
"flask": { "flask": {
"hashes": [ "hashes": [
"sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48",
"sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05"
"sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52",
"sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.0.2"
"version": "==1.1.1"
}, },
"flask-migrate": { "flask-migrate": {
"hashes": [ "hashes": [
"sha256:493f9b3795985b9b4915bf3b7d16946697f027b73545384e7d9e3a79f989d2fe",
"sha256:b709ca8642559c3c5a81a33ab10839fa052177accd5ba821047a99db635255ed"
"sha256:6fb038be63d4c60727d5dfa5f581a6189af5b4e2925bc378697b4f0a40cfb4e1",
"sha256:a96ff1875a49a40bd3e8ac04fce73fdb0870b9211e6168608cbafa4eb839d502"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.1"
"version": "==2.5.2"
}, },
"flask-sqlalchemy": { "flask-sqlalchemy": {
"hashes": [ "hashes": [
"sha256:3bc0fac969dd8c0ace01b32060f0c729565293302f0c4269beed154b46bec50b",
"sha256:5971b9852b5888655f11db634e87725a9031e170f37c0ce7851cf83497f56e53"
"sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327",
"sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.3.2"
"version": "==2.4.1"
}, },
"iso8601": { "iso8601": {
"hashes": [ "hashes": [
@ -103,156 +98,182 @@
}, },
"itsdangerous": { "itsdangerous": {
"hashes": [ "hashes": [
"sha256:cbb3fcf8d3e33df861709ecaf89d9e6629cff0a217bc2848f1b41cd30d360519"
"sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19",
"sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"
], ],
"version": "==0.24"
"version": "==1.1.0"
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
], ],
"version": "==2.10"
"version": "==2.10.3"
}, },
"mako": { "mako": {
"hashes": [ "hashes": [
"sha256:4e02fde57bd4abb5ec400181e4c314f56ac3e49ba4fb8b0d50bba18cb27d25ae"
"sha256:a36919599a9b7dc5d86a7a8988f23a9a3a3d083070023bab23d64f7f1d1e0a4b"
], ],
"version": "==1.0.7"
"version": "==1.1.0"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
}, },
"pycparser": { "pycparser": {
"hashes": [ "hashes": [
"sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226"
"sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"
], ],
"version": "==2.18"
"version": "==2.19"
}, },
"pynacl": { "pynacl": {
"hashes": [ "hashes": [
"sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca",
"sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512",
"sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6",
"sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776",
"sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac",
"sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b",
"sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb",
"sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98",
"sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd",
"sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2",
"sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef",
"sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f",
"sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988",
"sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b",
"sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415",
"sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2",
"sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101",
"sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0",
"sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582",
"sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a",
"sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975",
"sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1",
"sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb",
"sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45",
"sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031",
"sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9",
"sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752",
"sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0",
"sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c",
"sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053",
"sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4"
"sha256:05c26f93964373fc0abe332676cb6735f0ecad27711035b9472751faa8521255",
"sha256:0c6100edd16fefd1557da078c7a31e7b7d7a52ce39fdca2bec29d4f7b6e7600c",
"sha256:0d0a8171a68edf51add1e73d2159c4bc19fc0718e79dec51166e940856c2f28e",
"sha256:1c780712b206317a746ace34c209b8c29dbfd841dfbc02aa27f2084dd3db77ae",
"sha256:2424c8b9f41aa65bbdbd7a64e73a7450ebb4aa9ddedc6a081e7afcc4c97f7621",
"sha256:2d23c04e8d709444220557ae48ed01f3f1086439f12dbf11976e849a4926db56",
"sha256:30f36a9c70450c7878053fa1344aca0145fd47d845270b43a7ee9192a051bf39",
"sha256:37aa336a317209f1bb099ad177fef0da45be36a2aa664507c5d72015f956c310",
"sha256:4943decfc5b905748f0756fdd99d4f9498d7064815c4cf3643820c9028b711d1",
"sha256:57ef38a65056e7800859e5ba9e6091053cd06e1038983016effaffe0efcd594a",
"sha256:5bd61e9b44c543016ce1f6aef48606280e45f892a928ca7068fba30021e9b786",
"sha256:6482d3017a0c0327a49dddc8bd1074cc730d45db2ccb09c3bac1f8f32d1eb61b",
"sha256:7d3ce02c0784b7cbcc771a2da6ea51f87e8716004512493a2b69016326301c3b",
"sha256:a14e499c0f5955dcc3991f785f3f8e2130ed504fa3a7f44009ff458ad6bdd17f",
"sha256:a39f54ccbcd2757d1d63b0ec00a00980c0b382c62865b61a505163943624ab20",
"sha256:aabb0c5232910a20eec8563503c153a8e78bbf5459490c49ab31f6adf3f3a415",
"sha256:bd4ecb473a96ad0f90c20acba4f0bf0df91a4e03a1f4dd6a4bdc9ca75aa3a715",
"sha256:e2da3c13307eac601f3de04887624939aca8ee3c9488a0bb0eca4fb9401fc6b1",
"sha256:f67814c38162f4deb31f68d590771a29d5ae3b1bd64b75cf232308e5c74777e0"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.2.1"
"version": "==1.3.0"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
], ],
"version": "==2.7.3"
"version": "==2.8.0"
},
"python-dotenv": {
"hashes": [
"sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093",
"sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544"
],
"index": "pypi",
"version": "==0.10.3"
}, },
"python-editor": { "python-editor": {
"hashes": [ "hashes": [
"sha256:a3c066acee22a1c94f63938341d4fb374e3fdd69366ed6603d7b24bed1efc565"
"sha256:1bf6e860a8ad52a14c3ee1252d5dc25b2030618ed80c022598f00176adc8367d",
"sha256:51fda6bcc5ddbbb7063b2af7509e43bd84bfc32a4ff71349ec7847713882327b",
"sha256:5f98b069316ea1c2ed3f67e7f5df6c0d8f10b689964a4a811ff64f0106819ec8"
], ],
"version": "==1.0.3"
"version": "==1.0.4"
}, },
"rfc3339": { "rfc3339": {
"hashes": [ "hashes": [
"sha256:589c8b8cab8a35f85313cb80f1b0b0b3ca16a527f354beadb59882fd4473f187",
"sha256:a8167214d37449a6af9b463285baffc87a6d65e013507fd1ba63a48a60b62043"
"sha256:d53c3b5eefaef892b7240ba2a91fef012e86faa4d0a0ca782359c490e00ad4d0",
"sha256:f44316b21b21db90a625cde04ebb0d46268f153e6093021fa5893e92a96f58a3"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.0"
"version": "==6.2"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
], ],
"version": "==1.11.0"
"version": "==1.12.0"
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
"sha256:72325e67fb85f6e9ad304c603d83626d1df684fdf0c7ab1f0352e71feeab69d8"
"sha256:272a835758908412e75e87f75dd0179a51422715c125ce42109632910526b1fd"
], ],
"version": "==1.2.10"
"version": "==1.3.9"
}, },
"werkzeug": { "werkzeug": {
"hashes": [ "hashes": [
"sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c",
"sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b"
"sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7",
"sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"
], ],
"version": "==0.14.1"
"version": "==0.16.0"
} }
}, },
"develop": { "develop": {
"alabaster": { "alabaster": {
"hashes": [ "hashes": [
"sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456",
"sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
], ],
"version": "==0.7.11"
"version": "==0.7.12"
}, },
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:0a0c484279a5f08c9bcedd6fa9b42e378866a7dcc695206b92d59dc9f2d9760d",
"sha256:218e36cf8d98a42f16214e8670819ce307fa707d1dcf7f9af84c7aede1febc7f"
"sha256:98c665ad84d10b18318c5ab7c3d203fe11714cbad2a4aef4f44651f415392754",
"sha256:b7546ffdedbf7abcfbff93cd1de9e9980b1ef744852689decc5aeada324238c6"
], ],
"version": "==2.0.1"
"version": "==2.3.1"
}, },
"atomicwrites": { "atomicwrites": {
"hashes": [ "hashes": [
"sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585",
"sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6"
"sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
"sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
], ],
"version": "==1.1.5"
"version": "==1.3.0"
}, },
"attrs": { "attrs": {
"hashes": [ "hashes": [
"sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265",
"sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b"
"sha256:ec20e7a4825331c1b5ebf261d111e16fa9612c1f7a5e1f884f12bd53a664dfd2",
"sha256:f913492e1663d3c36f502e5e9ba6cd13cf19d7fab50aa13239e420fef95e1396"
], ],
"version": "==18.1.0"
"version": "==19.2.0"
}, },
"babel": { "babel": {
"hashes": [ "hashes": [
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
"sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
"sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
], ],
"version": "==2.6.0"
"version": "==2.7.0"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7",
"sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0"
"sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50",
"sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"
], ],
"version": "==2018.4.16"
"version": "==2019.9.11"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@ -261,58 +282,59 @@
], ],
"version": "==3.0.4" "version": "==3.0.4"
}, },
"colorama": {
"hashes": [
"sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
"sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
],
"markers": "sys_platform == 'win32'",
"version": "==0.4.1"
},
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
"sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
"sha256:104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a",
"sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
"sha256:15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd",
"sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
"sha256:1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2",
"sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
"sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162",
"sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1",
"sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508",
"sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249",
"sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694",
"sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a",
"sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287",
"sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1",
"sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000",
"sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1",
"sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e",
"sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5",
"sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062",
"sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba",
"sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc",
"sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc",
"sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99",
"sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653",
"sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c",
"sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558",
"sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f",
"sha256:9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4",
"sha256:ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91",
"sha256:b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d",
"sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9",
"sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
"sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
"sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
"sha256:e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77",
"sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80",
"sha256:f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"
"sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6",
"sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650",
"sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5",
"sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d",
"sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351",
"sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755",
"sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef",
"sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca",
"sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca",
"sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9",
"sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc",
"sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5",
"sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f",
"sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe",
"sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888",
"sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5",
"sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce",
"sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5",
"sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e",
"sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e",
"sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9",
"sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437",
"sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1",
"sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c",
"sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24",
"sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47",
"sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2",
"sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28",
"sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c",
"sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7",
"sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0",
"sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.5.1"
"version": "==4.5.4"
}, },
"docutils": { "docutils": {
"hashes": [ "hashes": [
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
"sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0",
"sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827",
"sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99"
], ],
"version": "==0.14"
"version": "==0.15.2"
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
@ -323,72 +345,95 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
], ],
"version": "==2.7"
"version": "==2.8"
}, },
"imagesize": { "imagesize": {
"hashes": [ "hashes": [
"sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18",
"sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315"
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
],
"version": "==1.1.0"
},
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
], ],
"version": "==1.0.0"
"markers": "python_version < '3.8'",
"version": "==0.23"
}, },
"isort": { "isort": {
"hashes": [ "hashes": [
"sha256:1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af",
"sha256:b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8",
"sha256:ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
], ],
"version": "==4.3.4"
"version": "==4.3.21"
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
], ],
"version": "==2.10"
"version": "==2.10.3"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
"hashes": [ "hashes": [
"sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33",
"sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39",
"sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019",
"sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088",
"sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b",
"sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e",
"sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6",
"sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b",
"sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5",
"sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff",
"sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd",
"sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7",
"sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff",
"sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d",
"sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2",
"sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35",
"sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4",
"sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514",
"sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252",
"sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109",
"sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f",
"sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c",
"sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92",
"sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577",
"sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d",
"sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d",
"sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f",
"sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a",
"sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b"
],
"version": "==1.3.1"
"sha256:02b260c8deb80db09325b99edf62ae344ce9bc64d68b7a634410b8e9a568edbf",
"sha256:18f9c401083a4ba6e162355873f906315332ea7035803d0fd8166051e3d402e3",
"sha256:1f2c6209a8917c525c1e2b55a716135ca4658a3042b5122d4e3413a4030c26ce",
"sha256:2f06d97f0ca0f414f6b707c974aaf8829c2292c1c497642f63824119d770226f",
"sha256:616c94f8176808f4018b39f9638080ed86f96b55370b5a9463b2ee5c926f6c5f",
"sha256:63b91e30ef47ef68a30f0c3c278fbfe9822319c15f34b7538a829515b84ca2a0",
"sha256:77b454f03860b844f758c5d5c6e5f18d27de899a3db367f4af06bec2e6013a8e",
"sha256:83fe27ba321e4cfac466178606147d3c0aa18e8087507caec78ed5a966a64905",
"sha256:84742532d39f72df959d237912344d8a1764c2d03fe58beba96a87bfa11a76d8",
"sha256:874ebf3caaf55a020aeb08acead813baf5a305927a71ce88c9377970fe7ad3c2",
"sha256:9f5caf2c7436d44f3cec97c2fa7791f8a675170badbfa86e1992ca1b84c37009",
"sha256:a0c8758d01fcdfe7ae8e4b4017b13552efa7f1197dd7358dc9da0576f9d0328a",
"sha256:a4def978d9d28cda2d960c279318d46b327632686d82b4917516c36d4c274512",
"sha256:ad4f4be843dace866af5fc142509e9b9817ca0c59342fdb176ab6ad552c927f5",
"sha256:ae33dd198f772f714420c5ab698ff05ff900150486c648d29951e9c70694338e",
"sha256:b4a2b782b8a8c5522ad35c93e04d60e2ba7f7dcb9271ec8e8c3e08239be6c7b4",
"sha256:c462eb33f6abca3b34cdedbe84d761f31a60b814e173b98ede3c81bb48967c4f",
"sha256:fd135b8d35dfdcdb984828c84d695937e58cc5f49e1c854eb311c4d6aa03f4f1"
],
"version": "==1.4.2"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [
"sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665"
],
"version": "==1.0"
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
],
"version": "==1.1.1"
}, },
"mccabe": { "mccabe": {
"hashes": [ "hashes": [
@ -399,159 +444,151 @@
}, },
"mock": { "mock": {
"hashes": [ "hashes": [
"sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
"sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
"sha256:83657d894c90d5681d62155c82bda9c1187827525880eda8ff5df4ec813437c3",
"sha256:d157e52d4e5b938c550f39eb2fd15610db062441a9c2747d3dbfa9298211d0f8"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.0"
"version": "==3.0.5"
}, },
"more-itertools": { "more-itertools": {
"hashes": [ "hashes": [
"sha256:2b6b9893337bfd9166bee6a62c2b0c9fe7735dcf85948b387ec8cba30e85d8e8",
"sha256:6703844a52d3588f951883005efcf555e49566a48afd4db4e965d69b883980d3",
"sha256:a18d870ef2ffca2b8463c0070ad17b5978056f403fb64e3f15fe62a52db21cc0"
"sha256:409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832",
"sha256:92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"
], ],
"version": "==4.2.0"
"version": "==7.2.0"
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:673ea75fb750289b7d1da1331c125dc62fc1c3a8db9129bb372ae7b7d5bf300a",
"sha256:c770605a579fdd4a014e9f0a34b6c7a36ce69b08100ff728e96e27445cef3b3c"
"sha256:1d98fd818ad3128a5408148c9e4a5edce6ed6b58cc314283e631dd5d9216527b",
"sha256:22ee018e8fc212fe601aba65d3699689dd29a26410ef0d2cc1943de7bec7e3ac",
"sha256:3a24f80776edc706ec8d05329e854d5b9e464cd332e25cde10c8da2da0a0db6c",
"sha256:42a78944e80770f21609f504ca6c8173f7768043205b5ac51c9144e057dcf879",
"sha256:4b2b20106973548975f0c0b1112eceb4d77ed0cafe0a231a1318f3b3a22fc795",
"sha256:591a9625b4d285f3ba69f541c84c0ad9e7bffa7794da3fa0585ef13cf95cb021",
"sha256:5b4b70da3d8bae73b908a90bb2c387b977e59d484d22c604a2131f6f4397c1a3",
"sha256:84edda1ffeda0941b2ab38ecf49302326df79947fa33d98cdcfbf8ca9cf0bb23",
"sha256:b2b83d29babd61b876ae375786960a5374bba0e4aba3c293328ca6ca5dc448dd",
"sha256:cc4502f84c37223a1a5ab700649b5ab1b5e4d2bf2d426907161f20672a21930b",
"sha256:e29e24dd6e7f39f200a5bb55dcaa645d38a397dd5a6674f6042ef02df5795046"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.620"
"version": "==0.730"
}, },
"packaging": {
"mypy-extensions": {
"hashes": [ "hashes": [
"sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0",
"sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"
"sha256:a161e3b917053de87dbe469987e173e49fb454eca10ef28b48b384538cc11458"
], ],
"version": "==17.1"
"version": "==0.4.2"
}, },
"pbr": {
"packaging": {
"hashes": [ "hashes": [
"sha256:1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45",
"sha256:b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"
"sha256:28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47",
"sha256:d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"
], ],
"version": "==4.2.0"
"version": "==19.2"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
"sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
"sha256:0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6",
"sha256:fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"
], ],
"version": "==0.7.1"
"version": "==0.13.0"
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
"sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa",
"sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"
], ],
"version": "==1.5.4"
"version": "==1.8.0"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
"sha256:74abc4e221d393ea5ce1f129ea6903209940c1ecd29e002e8c6933c2b21026e0",
"sha256:cbc619d09254895b0d12c2c691e237b2e91e9b2ecf5e84c26b35400f93dcfb83",
"sha256:cbfca99bd594a10f674d0cd97a3d802a1fdef635d4361e1a2658de47ed261e3a"
"sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
"sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.4.0"
"version": "==2.5.0"
}, },
"pydocstyle": { "pydocstyle": {
"hashes": [ "hashes": [
"sha256:08a870edc94508264ed90510db466c6357c7192e0e866561d740624a8fc7d90c",
"sha256:4d5bcde961107873bae621f3d580c3e35a426d3687ffc6f8fb356f6628da5a97",
"sha256:af9fcccb303899b83bec82dc9a1d56c60fc369973223a5e80c3dfa9bdf984405"
"sha256:04c84e034ebb56eb6396c820442b8c4499ac5eb94a3bda88951ac3dc519b6058",
"sha256:66aff87ffe34b1e49bff2dd03a88ce6843be2f3346b0c9814410d34987fbab59"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.1"
"version": "==4.0.1"
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d",
"sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"
"sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127",
"sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"
], ],
"version": "==2.2.0"
"version": "==2.4.2"
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:2c90a24bee8fae22ac98061c896e61f45c5b73c2e0511a4bf53f99ba56e90434",
"sha256:454532779425098969b8f54ab0f056000b883909f69d05905ea114df886e3251"
"sha256:7edbae11476c2182708063ac387a8f97c760d9cfe36a5ede0ca996f90cf346c8",
"sha256:844ce067788028c1a35086a5c66bc5e599ddd851841c41d6ee1623b36774d9f2"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.0.1"
"version": "==2.4.2"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04",
"sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07",
"sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18",
"sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e",
"sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5",
"sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58",
"sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"
"sha256:6f98a7b9397e206d78cc01df10131398f1c8b8510a2f4d97d9abd82e1aacdd80",
"sha256:d9338df12903bbf5d65a0e4e87c2161968b10d2e489652bb47001d82a9b028b4"
], ],
"version": "==2.2.0"
"version": "==2.4.2"
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:341ec10361b64a24accaec3c7ba5f7d5ee1ca4cebea30f76fad3dd12db9f0541",
"sha256:952c0389db115437f966c4c2079ae9d54714b9455190e56acebe14e8c38a7efa"
"sha256:7e4800063ccfc306a53c461442526c5571e1462f61583506ce97e4da6a1d88c8",
"sha256:ca563435f4941d0cb34767301c27bc65c510cb82e90b9ecf9cb52dc2c63caaa0"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.6.4"
"version": "==5.2.1"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
"sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
"sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
],
"version": "==2.7.3"
},
"python-dotenv": {
"hashes": [
"sha256:4965ed170bf51c347a89820e8050655e9c25db3837db6602e906b6d850fad85c",
"sha256:509736185257111613009974e666568a1b031b028b61b500ef1ab4ee780089d5"
"sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb",
"sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"
], ],
"index": "pypi",
"version": "==0.8.2"
"version": "==2.8.0"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
"sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
], ],
"version": "==2018.5"
"version": "==2019.3"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1",
"sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
], ],
"version": "==2.19.1"
"version": "==2.22.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
], ],
"version": "==1.11.0"
"version": "==1.12.0"
}, },
"snowballstemmer": { "snowballstemmer": {
"hashes": [ "hashes": [
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
], ],
"version": "==1.2.1"
"version": "==2.0.0"
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:217ad9ece2156ed9f8af12b5d2c82a499ddf2c70a33c5f81864a08d8c67b9efc",
"sha256:a765c6db1e5b62aae857697cd4402a5c1a315a7b0854bbcd0fc8cdc524da5896"
"sha256:9f3e17c64b34afc653d7c5ec95766e03043cc6d80b0de224f59b6b6e19d37c3c",
"sha256:c7658aab75c920288a8cf6f09f244c6cfdae30d82d803ac1634d9f223a80ca08"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.7.6"
"version": "==1.8.5"
}, },
"sphinx-jsondomain": { "sphinx-jsondomain": {
"hashes": [ "hashes": [
@ -563,11 +600,11 @@
}, },
"sphinx-rtd-theme": { "sphinx-rtd-theme": {
"hashes": [ "hashes": [
"sha256:3b49758a64f8a1ebd8a33cb6cc9093c3935a908b716edfaa5772fd86aac27ef6",
"sha256:80e01ec0eb711abacb1fa507f3eae8b805ae8fa3e8b057abfdf497e3f644c82c"
"sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
"sha256:728607e34d60456d736cc7991fd236afb828b21b82f956c5ea75f94c8414040a"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.4.1"
"version": "==0.4.3"
}, },
"sphinxcontrib-httpdomain": { "sphinxcontrib-httpdomain": {
"hashes": [ "hashes": [
@ -579,51 +616,66 @@
}, },
"sphinxcontrib-websupport": { "sphinxcontrib-websupport": {
"hashes": [ "hashes": [
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
"sha256:1501befb0fdf1d1c29a800fdbf4ef5dc5369377300ddbdd16d2cd40e54c6eefc",
"sha256:e02f717baf02d0b6c3dd62cf81232ffca4c9d5c331e03766982e3ff9f1d2bc3f"
], ],
"version": "==1.1.0"
"version": "==1.1.2"
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
"sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58",
"sha256:10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d",
"sha256:1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291",
"sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a",
"sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9",
"sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892",
"sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9",
"sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded",
"sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa",
"sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe",
"sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd",
"sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85",
"sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6",
"sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46",
"sha256:898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51",
"sha256:94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f",
"sha256:a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129",
"sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c",
"sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea",
"sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863",
"sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559",
"sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87",
"sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"
],
"version": "==1.1.0"
"sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e",
"sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e",
"sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0",
"sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c",
"sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631",
"sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4",
"sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34",
"sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b",
"sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a",
"sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233",
"sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1",
"sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36",
"sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d",
"sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a",
"sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"
],
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.0"
},
"typing-extensions": {
"hashes": [
"sha256:2ed632b30bb54fc3941c382decfd0ee4148f5c591651c9272473fea2c6397d95",
"sha256:b1edbbf0652660e32ae780ac9433f4231e7339c7f9a8057d0f042fcbcea49b87",
"sha256:d8179012ec2c620d3791ca6fe2bf7979d979acdbef1fca0bc56b37411db682ed"
],
"version": "==3.7.4"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf",
"sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"
"sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398",
"sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"
], ],
"version": "==1.23"
"version": "==1.25.6"
},
"wcwidth": {
"hashes": [
"sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e",
"sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c"
],
"version": "==0.1.7"
}, },
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
"sha256:d4d560d479f2c21e1b5443bbd15fe7ec4b37fe7e53d335d3b9b0a7b1226fe3c6"
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"
],
"version": "==1.11.2"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
], ],
"version": "==1.10.11"
"version": "==0.6.0"
} }
} }
} }

3
server/corvus/__init__.py

@ -81,9 +81,10 @@ def register_blueprints(app: Flask) -> None:
:param app: :param app:
:return: :return:
""" """
from corvus.api import AUTH_BLUEPRINT, USER_BLUEPRINT
from corvus.api import AUTH_BLUEPRINT, USER_BLUEPRINT, HEALTH_BLUEPRINT
app.register_blueprint(AUTH_BLUEPRINT) app.register_blueprint(AUTH_BLUEPRINT)
app.register_blueprint(USER_BLUEPRINT) app.register_blueprint(USER_BLUEPRINT)
app.register_blueprint(HEALTH_BLUEPRINT)
def register_error_handlers(app: Flask) -> None: def register_error_handlers(app: Flask) -> None:

1
server/corvus/api/__init__.py

@ -1,3 +1,4 @@
"""API blueprint exports.""" """API blueprint exports."""
from corvus.api.authentication_api import AUTH_BLUEPRINT from corvus.api.authentication_api import AUTH_BLUEPRINT
from corvus.api.user_api import USER_BLUEPRINT from corvus.api.user_api import USER_BLUEPRINT
from corvus.api.health_api import HEALTH_BLUEPRINT

95
server/corvus/api/authentication_api.py

@ -1,14 +1,19 @@
"""Authentication API blueprint and endpoint definitions.""" """Authentication API blueprint and endpoint definitions."""
from flask import Blueprint, g
from flask import Blueprint, g, abort, request
from corvus.api.decorators import return_json from corvus.api.decorators import return_json
from corvus.api.model import APIMessage, APIResponse
from corvus.api.model import APIMessage, APIResponse, APIPage
from corvus.middleware import authentication_middleware from corvus.middleware import authentication_middleware
from corvus.service import ( from corvus.service import (
user_token_service, user_token_service,
authentication_service, authentication_service,
user_service
user_service,
transformation_service
) )
from corvus.middleware.authentication_middleware import Auth
from corvus.service.role_service import Role
from corvus.model import UserToken
from corvus.utility.pagination_utility import get_pagination_params
AUTH_BLUEPRINT = Blueprint( AUTH_BLUEPRINT = Blueprint(
name='auth', import_name=__name__, url_prefix='/auth') name='auth', import_name=__name__, url_prefix='/auth')
@ -16,7 +21,8 @@ AUTH_BLUEPRINT = Blueprint(
@AUTH_BLUEPRINT.route('/login', methods=['POST']) @AUTH_BLUEPRINT.route('/login', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_basic_auth
@authentication_middleware.require(
required_auth=Auth.BASIC, required_role=Role.USER)
def login() -> APIResponse: def login() -> APIResponse:
""" """
Get a token for continued authentication. Get a token for continued authentication.
@ -29,7 +35,8 @@ def login() -> APIResponse:
@AUTH_BLUEPRINT.route('/bump', methods=['POST']) @AUTH_BLUEPRINT.route('/bump', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.USER)
def login_bump() -> APIResponse: def login_bump() -> APIResponse:
""" """
Update the user last seen timestamp. Update the user last seen timestamp.
@ -42,7 +49,8 @@ def login_bump() -> APIResponse:
@AUTH_BLUEPRINT.route('/logout', methods=['POST']) @AUTH_BLUEPRINT.route('/logout', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.USER)
def logout() -> APIResponse: def logout() -> APIResponse:
""" """
Logout and delete a token. Logout and delete a token.
@ -51,3 +59,78 @@ def logout() -> APIResponse:
""" """
authentication_service.logout(g.user_token) authentication_service.logout(g.user_token)
return APIResponse(APIMessage(True, None), 200) return APIResponse(APIMessage(True, None), 200)
@AUTH_BLUEPRINT.route('/token', methods=['GET'])
@return_json
@authentication_middleware.require(
required_auth=Auth.BASIC, required_role=Role.USER)
def get_tokens() -> APIResponse:
"""
Get a list of all tokens for the current user.
:return: a paginated list of user tokens
"""
page, per_page = get_pagination_params(request.args)
user_token_page = user_token_service.find_by_user(g.user, page, per_page)
api_page = APIPage.from_page(user_token_page)
if api_page is not None:
return APIResponse(api_page, 200)
return abort(404)
@AUTH_BLUEPRINT.route('/token', methods=['POST'])
@return_json
@authentication_middleware.require(
required_auth=Auth.BASIC, required_role=Role.USER)
def create_token() -> APIResponse:
"""
Create a new token with optional parameters.
note: String
enabled: Boolean
expirationTime: DateTime
:return: The new token with the optional parameters
"""
requested_token: UserToken = transformation_service.deserialize_model(
UserToken, request.json, options=['note', 'enabled', 'expirationTime'])
user_token = user_token_service.create(
g.user, requested_token.note,
requested_token.enabled, requested_token.expiration_time)
return APIResponse(user_token, 200)
@AUTH_BLUEPRINT.route('/token/<token>', methods=['GET'])
@return_json
@authentication_middleware.require(
required_auth=Auth.BASIC, required_role=Role.USER)
def get_token(token: str) -> APIResponse:
"""
Retrieve a specific token for this user.
:param token: The token to retrieve for this user
:return: The token if it exists
"""
user_token = user_token_service.find_by_user_and_token(g.user, token)
if user_token is None:
return abort(404)
return APIResponse(user_token, 200)
@AUTH_BLUEPRINT.route('/token/<token>', methods=['DELETE'])
@return_json
@authentication_middleware.require(
required_auth=Auth.BASIC, required_role=Role.USER)
def delete_token(token: str) -> APIResponse:
"""
Delete a specific token for this user.
:param token: The token to delete for this user
:return: Nothing on success
"""
user_token = user_token_service.find_by_user_and_token(g.user, token)
if user_token is None:
return abort(404)
user_token_service.delete(user_token)
return APIResponse(APIMessage(True, None), 200)

22
server/corvus/api/health_api.py

@ -0,0 +1,22 @@
"""Endpoint to expose the health of the application."""
from flask import Blueprint
from corvus.api.decorators import return_json
from corvus.api.model import APIResponse, APIMessage
HEALTH_BLUEPRINT = Blueprint(
name='health', import_name=__name__, url_prefix='/health')
@HEALTH_BLUEPRINT.route('', methods=['GET'])
@return_json
def get_health() -> APIResponse:
"""
Retrieve the health for the service.
:return:
"""
return APIResponse(
APIMessage(True, 'Service is healthy'),
200
)

15
server/corvus/api/model.py

@ -1,9 +1,9 @@
"""Model definitions for the api module.""" """Model definitions for the api module."""
from typing import Any, List, Optional, Dict, Type
from typing import Any, List, Optional, Dict
from flask_sqlalchemy import Pagination from flask_sqlalchemy import Pagination
from corvus import db
from corvus.db import db_model
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
@ -61,12 +61,12 @@ class APIPage(BaseAPIMessage):
page: int, page: int,
total_count: int, total_count: int,
last_page: int, last_page: int,
items: List[Type[db.Model]]) -> None:
items: List[db_model]) -> None:
"""Construct and APIPage.""" """Construct and APIPage."""
self.page = page self.page = page
self.count = len(items) self.count = len(items)
self.total_count = total_count self.total_count = total_count
self.last_page = last_page
self.last_page = last_page if last_page > 0 else page
self.items = items self.items = items
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
@ -79,7 +79,12 @@ class APIPage(BaseAPIMessage):
'items': self.items 'items': self.items
} }
def is_empty(self) -> bool:
"""Check if the page is empty."""
return self.count == 0 and self.total_count == 0
@staticmethod @staticmethod
def from_page(page: Pagination) -> 'APIPage': def from_page(page: Pagination) -> 'APIPage':
"""Create an APIPage from a Pagination object.""" """Create an APIPage from a Pagination object."""
return APIPage(page.page, page.total, page.pages, page.items)
page = APIPage(page.page, page.total, page.pages, page.items)
return page if not page.is_empty() else None

38
server/corvus/api/user_api.py

@ -5,6 +5,7 @@ from flask import Blueprint, abort, request, g
from corvus.api.decorators import return_json from corvus.api.decorators import return_json
from corvus.api.model import APIResponse, APIMessage, APIPage from corvus.api.model import APIResponse, APIMessage, APIPage
from corvus.middleware import authentication_middleware from corvus.middleware import authentication_middleware
from corvus.middleware.authentication_middleware import Auth
from corvus.model import User from corvus.model import User
from corvus.service import ( from corvus.service import (
patch_service, patch_service,
@ -14,6 +15,7 @@ from corvus.service import (
from corvus.service.patch_service import get_patch_fields from corvus.service.patch_service import get_patch_fields
from corvus.service.role_service import Role from corvus.service.role_service import Role
from corvus.utility.pagination_utility import get_pagination_params from corvus.utility.pagination_utility import get_pagination_params
from corvus.service.role_service import ROLE_LIST
USER_BLUEPRINT = Blueprint( USER_BLUEPRINT = Blueprint(
name='user', import_name=__name__, url_prefix='/user') name='user', import_name=__name__, url_prefix='/user')
@ -21,8 +23,8 @@ USER_BLUEPRINT = Blueprint(
@USER_BLUEPRINT.route('', methods=['GET']) @USER_BLUEPRINT.route('', methods=['GET'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.USER)
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.USER)
def get_users() -> APIResponse: def get_users() -> APIResponse:
""" """
Get a list of users. Get a list of users.
@ -38,8 +40,8 @@ def get_users() -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['GET']) @USER_BLUEPRINT.route('/<name>', methods=['GET'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.USER)
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.USER)
def get_user(name: str) -> APIResponse: def get_user(name: str) -> APIResponse:
""" """
Get a user. Get a user.
@ -54,8 +56,8 @@ def get_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['PATCH']) @USER_BLUEPRINT.route('/<name>', methods=['PATCH'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.USER)
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.USER)
def patch_user(name: str) -> APIResponse: def patch_user(name: str) -> APIResponse:
""" """
Patch a user. Patch a user.
@ -74,8 +76,8 @@ def patch_user(name: str) -> APIResponse:
@USER_BLUEPRINT.route('', methods=['POST']) @USER_BLUEPRINT.route('', methods=['POST'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.ADMIN)
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.ADMIN)
def register_user() -> APIResponse: def register_user() -> APIResponse:
""" """
Register a user with the service. Register a user with the service.
@ -99,8 +101,8 @@ def register_user() -> APIResponse:
@USER_BLUEPRINT.route('/<name>', methods=['DELETE']) @USER_BLUEPRINT.route('/<name>', methods=['DELETE'])
@return_json @return_json
@authentication_middleware.require_token_auth
@authentication_middleware.require_role(required_role=Role.ADMIN)
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.ADMIN)
def delete_user(name: str) -> APIResponse: def delete_user(name: str) -> APIResponse:
""" """
Delete a user with the service. Delete a user with the service.
@ -113,3 +115,19 @@ def delete_user(name: str) -> APIResponse:
return APIResponse( return APIResponse(
APIMessage(True, 'Successfully Deleted'), status=200) APIMessage(True, 'Successfully Deleted'), status=200)
return abort(404) return abort(404)
@USER_BLUEPRINT.route('/roles', methods=['GET'])
@return_json
@authentication_middleware.require(
required_auth=Auth.TOKEN, required_role=Role.USER)
def get_roles() -> APIResponse:
"""
List the roles available on the service.
:return: The list of roles
"""
return APIResponse(
sorted({str(role.data) for role in ROLE_LIST}),
status=200
)

3
server/corvus/db.py

@ -1,8 +1,11 @@
"""Database configuration and methods.""" """Database configuration and methods."""
from flask_migrate import upgrade from flask_migrate import upgrade
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.declarative import DeclarativeMeta
db: SQLAlchemy = SQLAlchemy() db: SQLAlchemy = SQLAlchemy()
db_model: DeclarativeMeta = db.Model
def init_db() -> None: def init_db() -> None:

5
server/corvus/errors.py

@ -43,14 +43,13 @@ class ClientError(BaseError):
class ValidationError(ClientError): class ValidationError(ClientError):
"""Corvus Validation Error.""" """Corvus Validation Error."""
pass
@return_json @return_json
def handle_corvus_404_error(exception: HTTPException) -> APIResponse: def handle_corvus_404_error(exception: HTTPException) -> APIResponse:
"""Error handler for 404 Corvus errors.""" """Error handler for 404 Corvus errors."""
return APIResponse( return APIResponse(
payload=APIMessage(False, 'Not Found'), status=exception.code)
payload=APIMessage(False, 'Not Found'),
status=exception.code if exception.code is not None else 404)
@return_json @return_json

71
server/corvus/middleware/authentication_middleware.py

@ -1,6 +1,7 @@
"""Middleware to handle authentication.""" """Middleware to handle authentication."""
import base64 import base64
import binascii import binascii
from enum import Enum
from functools import wraps from functools import wraps
from typing import Optional, Callable, Any from typing import Optional, Callable, Any
@ -8,15 +9,26 @@ from flask import request, Response, g, json
from werkzeug.datastructures import Authorization from werkzeug.datastructures import Authorization
from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes from werkzeug.http import bytes_to_wsgi, wsgi_to_bytes
from corvus.api.model import APIMessage
from corvus.service import ( from corvus.service import (
authentication_service, authentication_service,
user_service, user_service,
user_token_service user_token_service
) )
from corvus.service.role_service import ROLES, Role from corvus.service.role_service import ROLES, Role
from corvus.service import transformation_service
def authenticate_with_password(name: str, password: str) -> bool:
class Auth(Enum):
"""Authentication scheme definitions."""
TOKEN = 'TOKEN'
BASIC = 'BASIC'
NONE = 'NONE'
def authenticate_with_password(
name: Optional[str], password: Optional[str]) -> bool:
""" """
Authenticate a username and a password. Authenticate a username and a password.
@ -24,6 +36,8 @@ def authenticate_with_password(name: str, password: str) -> bool:
:param password: :param password:
:return: :return:
""" """
if name is None or password is None:
return False
user = user_service.find_by_name(name) user = user_service.find_by_name(name)
if user is not None \ if user is not None \
and authentication_service.is_valid_password(user, password): and authentication_service.is_valid_password(user, password):
@ -32,7 +46,7 @@ def authenticate_with_password(name: str, password: str) -> bool:
return False return False
def authenticate_with_token(name: str, token: str) -> bool:
def authenticate_with_token(name: Optional[str], token: Optional[str]) -> bool:
""" """
Authenticate a username and a token. Authenticate a username and a token.
@ -40,11 +54,13 @@ def authenticate_with_token(name: str, token: str) -> bool:
:param token: :param token:
:return: :return:
""" """
if name is None or token is None:
return False
user = user_service.find_by_name(name) user = user_service.find_by_name(name)
if user is not None: if user is not None:
user_token = user_token_service.find_by_user_and_token(user, token) user_token = user_token_service.find_by_user_and_token(user, token)
if user is not None \ if user is not None \
and authentication_service.is_valid_token(user_token):
and user_token_service.is_valid_token(user_token):
g.user = user g.user = user
g.user_token = user_token g.user_token = user_token
return True return True
@ -77,7 +93,7 @@ def authorization_failed(required_role: str) -> Response:
def parse_token_header( def parse_token_header(
header_value: str) -> Optional[Authorization]:
header_value: Optional[str]) -> Optional[Authorization]:
""" """
Parse the Authorization: Token header for the username and token. Parse the Authorization: Token header for the username and token.
@ -86,20 +102,13 @@ def parse_token_header(
""" """
if not header_value: if not header_value:
return None return None
value = wsgi_to_bytes(header_value)
try:
auth_type, auth_info = value.split(None, 1)
auth_type = auth_type.lower()
except ValueError:
return None
if auth_type == b'token':
auth_info = wsgi_to_bytes(header_value)
try: try:
username, token = base64.b64decode(auth_info).split(b':', 1) username, token = base64.b64decode(auth_info).split(b':', 1)
except binascii.Error: except binascii.Error:
return None return None
return Authorization('token', {'username': bytes_to_wsgi(username), return Authorization('token', {'username': bytes_to_wsgi(username),
'password': bytes_to_wsgi(token)}) 'password': bytes_to_wsgi(token)})
return None
def require_basic_auth(func: Callable) -> Callable: def require_basic_auth(func: Callable) -> Callable:
@ -143,7 +152,7 @@ def require_token_auth(func: Callable) -> Callable:
:return: :return:
""" """
token = parse_token_header( token = parse_token_header(
request.headers.get('Authorization', None))
request.headers.get('X-Auth-Token'))
if token and authenticate_with_token(token.username, token.password): if token and authenticate_with_token(token.username, token.password):
return func(*args, **kwargs) return func(*args, **kwargs)
return authentication_failed('Token') return authentication_failed('Token')
@ -152,7 +161,12 @@ def require_token_auth(func: Callable) -> Callable:
def require_role(required_role: Role) -> Callable: def require_role(required_role: Role) -> Callable:
"""Decorate require user role."""
"""
Decorate require a user role.
:param required_role:
:return:
"""
def required_role_decorator(func: Callable) -> Callable: def required_role_decorator(func: Callable) -> Callable:
"""Decorate the function.""" """Decorate the function."""
@wraps(func) @wraps(func)
@ -163,3 +177,32 @@ def require_role(required_role: Role) -> Callable:
return authorization_failed(required_role.value) return authorization_failed(required_role.value)
return decorate return decorate
return required_role_decorator return required_role_decorator
def require(required_auth: Auth, required_role: Role) -> Callable:
"""
Decorate require Auth and Role.
:param required_auth:
:param required_role:
:return:
"""
def require_decorator(func: Callable) -> Callable:
@wraps(func)
def decorate(*args: list, **kwargs: dict) -> Any:
decorated = require_role(required_role)(func)
if required_auth == Auth.BASIC:
decorated = require_basic_auth(decorated)
elif required_auth == Auth.TOKEN:
decorated = require_token_auth(decorated)
else:
return Response(
response=transformation_service.serialize_model(
APIMessage(
message="Unexpected Server Error",
success=False
)),
status=500)
return decorated(*args, **kwargs)
return decorate
return require_decorator

4
server/corvus/model/user_model.py

@ -10,8 +10,8 @@ class User(db.Model): # pylint: disable=too-few-public-methods
__tablename__ = 'user' __tablename__ = 'user'
ROLE_USER = 'USER'
ROLE_ADMIN = 'ADMIN'
ROLE_USER = Role.USER.value
ROLE_ADMIN = Role.ADMIN.value
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode(60), unique=True, nullable=False) name = db.Column(db.Unicode(60), unique=True, nullable=False)

21
server/corvus/service/authentication_service.py

@ -1,6 +1,5 @@
"""Service to handle authentication.""" """Service to handle authentication."""
import re import re
from datetime import datetime
from typing import Optional from typing import Optional
from nacl import pwhash from nacl import pwhash
@ -53,26 +52,6 @@ def is_valid_password(user: User, password: str) -> bool:
return False return False
def is_valid_token(user_token: Optional[UserToken]) -> bool:
"""
Validate a token.
Token must be enabled and if it has an expiration, it must be greater
than now.
:param user_token:
:return:
"""
if user_token is None:
return False
if not user_token.enabled:
return False
if (user_token.expiration_time is not None
and user_token.expiration_time < datetime.utcnow()):
return False
return True
def logout(user_token: Optional[UserToken] = None) -> None: def logout(user_token: Optional[UserToken] = None) -> None:
""" """
Remove a user_token associated with a client session. Remove a user_token associated with a client session.

23
server/corvus/service/patch_service.py

@ -1,8 +1,8 @@
"""Patching support for db.Model objects.""" """Patching support for db.Model objects."""
from typing import Type, Set, Optional, Any, Dict
from typing import Set, Optional, Any, Dict
from corvus import db
from corvus import errors from corvus import errors
from corvus.db import db_model
from corvus.model import User from corvus.model import User
from corvus.service import transformation_service from corvus.service import transformation_service
from corvus.service import validation_service from corvus.service import validation_service
@ -16,11 +16,11 @@ def get_patch_fields(patch_json: Dict[str, Any]) -> Set[str]:
def perform_patch(request_user: User, def perform_patch(request_user: User,
original_model: Type[db.Model],
patch_model: Type[db.Model],
original_model: db_model,
patch_model: db_model,
model_attributes: Set[str], model_attributes: Set[str],
patched_fields: Optional[Set[str]]) \ patched_fields: Optional[Set[str]]) \
-> Type[db.Model]:
-> db_model:
""" """
Patch changed attributes onto original model. Patch changed attributes onto original model.
@ -38,7 +38,6 @@ def perform_patch(request_user: User,
if model_validation.success: if model_validation.success:
for attribute, value in change_set.items(): for attribute, value in change_set.items():
setattr(original_model, attribute, value) setattr(original_model, attribute, value)
db.session.commit()
else: else:
raise errors.ValidationError( raise errors.ValidationError(
'Restricted attributes modified. Invalid Patch Set.') 'Restricted attributes modified. Invalid Patch Set.')
@ -46,11 +45,11 @@ def perform_patch(request_user: User,
def versioning_aware_patch(request_user: User, def versioning_aware_patch(request_user: User,
original_model: Type[db.Model],
patch_model: Type[db.Model],
original_model: db_model,
patch_model: db_model,
model_attributes: Set[str], model_attributes: Set[str],
patched_fields: Optional[Set[str]]) \ patched_fields: Optional[Set[str]]) \
-> Type[db.Model]:
-> db_model:
""" """
Account for version numbers in the model. Account for version numbers in the model.
@ -80,9 +79,9 @@ def versioning_aware_patch(request_user: User,
def patch( def patch(
request_user: User, request_user: User,
original_model: Type[db.Model],
patch_model: Type[db.Model],
patched_fields: Optional[Set[str]] = None) -> Type[db.Model]:
original_model: db_model,
patch_model: db_model,
patched_fields: Optional[Set[str]] = None) -> db_model:
""" """
Patch the original model with the patch model data. Patch the original model with the patch model data.

39
server/corvus/service/role_service.py

@ -7,6 +7,7 @@ from typing import Optional, List, Set, Dict
class Role(Enum): class Role(Enum):
"""User role definitions.""" """User role definitions."""
OWNER = 'OWNER'
ADMIN = 'ADMIN' ADMIN = 'ADMIN'
AUDITOR = 'AUDITOR' AUDITOR = 'AUDITOR'
MODERATOR = 'MODERATOR' MODERATOR = 'MODERATOR'
@ -14,39 +15,49 @@ class Role(Enum):
ANONYMOUS = 'ANONYMOUS' ANONYMOUS = 'ANONYMOUS'
NONE = 'NONE' NONE = 'NONE'
def __str__(self) -> str:
"""Return the value of the enum."""
return self.value
class RoleTree(defaultdict): class RoleTree(defaultdict):
"""Simple tree structure to handle hierarchy.""" """Simple tree structure to handle hierarchy."""
def __call__(self, data: Role) -> 'RoleTree':
def __call__(self, data: Role, power: int) -> 'RoleTree':
"""Handle direct calls to the tree.""" """Handle direct calls to the tree."""
return RoleTree(self, data)
return RoleTree(self, data, power)
# def __hash__(self):
parent: Optional['RoleTree']
data: Role
power: int
roles: Dict[Role, List['RoleTree']]
def __init__( def __init__(
self, self,
parent: Optional['RoleTree'], parent: Optional['RoleTree'],
data: Role, data: Role,
power: int = None,
**kwargs: dict) -> None: **kwargs: dict) -> None:
"""Configure a RoleTree.""" """Configure a RoleTree."""
super().__init__(**kwargs) super().__init__(**kwargs)
self.parent: Optional[RoleTree] = parent self.parent: Optional[RoleTree] = parent
self.data: Role = data self.data: Role = data
self.power: int = power if power is not None else 1
self.default_factory = self # type: ignore self.default_factory = self # type: ignore
self.roles: Dict[Role, List[RoleTree]] = {data: [self]} self.roles: Dict[Role, List[RoleTree]] = {data: [self]}
def populate( def populate(
self, children: Dict[Role, Optional[dict]]) -> List['RoleTree']: self, children: Dict[Role, Optional[dict]]) -> List['RoleTree']:
"""Populate a RoleTree from a dictionary of a Role hierarchy.""" """Populate a RoleTree from a dictionary of a Role hierarchy."""
role_list: List[RoleTree] = []
role_list: List[RoleTree] = [self]
for child_role in children.keys(): for child_role in children.keys():
element = children[child_role] element = children[child_role]
new_node = self(child_role)
new_node = self(child_role, self.power + 1)
if isinstance(element, dict) and element: if isinstance(element, dict) and element:
role_list += new_node.populate(element)
self[child_role] = new_node
role_list.extend(new_node.populate(element))
else:
role_list.append(new_node) role_list.append(new_node)
self[child_role] = new_node
for role_tree in role_list: for role_tree in role_list:
if role_tree.data not in self.roles.keys(): if role_tree.data not in self.roles.keys():
self.roles[role_tree.data] = [] self.roles[role_tree.data] = []
@ -56,7 +67,7 @@ class RoleTree(defaultdict):
def find_role(self, request_role: Role) -> List['RoleTree']: def find_role(self, request_role: Role) -> List['RoleTree']:
"""Identify all instances of a role.""" """Identify all instances of a role."""
try: try:
return [role_tree for role_tree in self.roles[request_role]]
return self.roles[request_role]
except KeyError: except KeyError:
return [] return []
@ -94,9 +105,15 @@ class RoleTree(defaultdict):
roles.extend(role_tree.get_children_roles()) roles.extend(role_tree.get_children_roles())
return set(roles) return set(roles)
def __str__(self) -> str:
"""Represent the tree with the value of the node."""
return 'RoleTree.%s(%d)' % (self.data, self.power)
ROLES = RoleTree(None, Role.ADMIN)
ROLES = RoleTree(None, Role.OWNER, 0)
ROLE_LIST = sorted(
ROLES.populate({ ROLES.populate({
Role.ADMIN: {
Role.MODERATOR: { Role.MODERATOR: {
Role.USER: { Role.USER: {
Role.ANONYMOUS: None Role.ANONYMOUS: None
@ -105,4 +122,6 @@ ROLES.populate({
Role.AUDITOR: { Role.AUDITOR: {
Role.USER: None Role.USER: None
} }
})
}
}),
key=lambda rt: rt.power)

14
server/corvus/service/transformation_service.py

@ -4,7 +4,7 @@ import re
from typing import Dict, Callable, Any, List, Optional, Type from typing import Dict, Callable, Any, List, Optional, Type
from corvus import errors from corvus import errors
from corvus.db import db
from corvus.db import db_model
LOGGER = logging.getLogger(__name__) LOGGER = logging.getLogger(__name__)
@ -12,9 +12,9 @@ LOGGER = logging.getLogger(__name__)
class BaseTransformer: class BaseTransformer:
"""Base Model serializer.""" """Base Model serializer."""
type: Type[db.Model]
type: db_model
def __init__(self, model: Type[db.Model]) -> None:
def __init__(self, model: db_model) -> None:
"""Initialize the base serializer.""" """Initialize the base serializer."""
self.model = model self.model = model
@ -62,7 +62,7 @@ class BaseTransformer:
raise NotImplementedError() raise NotImplementedError()
def _deserializers( def _deserializers(
self) -> Dict[str, Callable[[db.Model, Any], None]]:
self) -> Dict[str, Callable[[db_model, Any], None]]:
"""Field definitions.""" """Field definitions."""
raise NotImplementedError() raise NotImplementedError()
@ -88,7 +88,7 @@ def register_transformer(
return model_serializer return model_serializer
def serialize_model(model_obj: db.Model,
def serialize_model(model_obj: db_model,
options: Optional[List[str]] = None) -> Any: options: Optional[List[str]] = None) -> Any:
"""Lookup a Model and hand off to the serializer.""" """Lookup a Model and hand off to the serializer."""
try: try:
@ -100,9 +100,9 @@ def serialize_model(model_obj: db.Model,
def deserialize_model( def deserialize_model(
model_type: Type[db.Model],
model_type: db_model,
json_model_object: dict, json_model_object: dict,
options: Optional[List[str]] = None) -> db.Model:
options: Optional[List[str]] = None) -> db_model:
"""Lookup a Model and hand it off to the deserializer.""" """Lookup a Model and hand it off to the deserializer."""
try: try:
transformer = _model_transformers[model_type.__name__] transformer = _model_transformers[model_type.__name__]

52
server/corvus/service/user_token_service.py

@ -3,7 +3,8 @@ import uuid
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, Callable, Any from typing import Optional, Dict, Callable, Any
from iso8601 import iso8601
from flask_sqlalchemy import Pagination
from iso8601 import iso8601, ParseError
from corvus.db import db from corvus.db import db
from corvus.model import User, UserToken from corvus.model import User, UserToken
@ -11,6 +12,7 @@ from corvus.service.transformation_service import (
BaseTransformer, BaseTransformer,
register_transformer register_transformer
) )
from corvus import errors
@register_transformer @register_transformer
@ -41,6 +43,7 @@ class UserTokenTransformer(BaseTransformer):
'expirationTime': self.serialize_expiration_time, 'expirationTime': self.serialize_expiration_time,
'creationTime': self.serialize_creation_time, 'creationTime': self.serialize_creation_time,
'lastUsageTime': self.serialize_last_usage_time, 'lastUsageTime': self.serialize_last_usage_time,
'isValid': self.serialize_is_valid,
'version': self.serialize_version 'version': self.serialize_version
} }
@ -79,7 +82,11 @@ class UserTokenTransformer(BaseTransformer):
def deserialize_expiration_time( def deserialize_expiration_time(
model: UserToken, expiration_time: datetime) -> None: model: UserToken, expiration_time: datetime) -> None:
"""User token expiration time.""" """User token expiration time."""
try:
model.expiration_time = iso8601.parse_date(expiration_time) model.expiration_time = iso8601.parse_date(expiration_time)
except ParseError:
raise errors.ValidationError(
'Cannot parse datetime from %s' % expiration_time)
def serialize_creation_time(self) -> datetime: def serialize_creation_time(self) -> datetime:
"""User token creation time.""" """User token creation time."""
@ -89,7 +96,11 @@ class UserTokenTransformer(BaseTransformer):
def deserialize_creation_time( def deserialize_creation_time(
model: UserToken, creation_time: datetime) -> None: model: UserToken, creation_time: datetime) -> None:
"""User token creation time.""" """User token creation time."""
try:
model.creation_time = iso8601.parse_date(creation_time) model.creation_time = iso8601.parse_date(creation_time)
except ParseError:
raise errors.ValidationError(
'Cannot parse datetime from %s' % creation_time)
def serialize_last_usage_time(self) -> datetime: def serialize_last_usage_time(self) -> datetime:
"""User token last usage time.""" """User token last usage time."""
@ -101,6 +112,10 @@ class UserTokenTransformer(BaseTransformer):
"""User token last usage time.""" """User token last usage time."""
model.last_usage_time = iso8601.parse_date(last_usage_time) model.last_usage_time = iso8601.parse_date(last_usage_time)
def serialize_is_valid(self) -> bool:
"""User token is_valid computed value."""
return is_valid_token(self.model)
def serialize_version(self) -> int: def serialize_version(self) -> int:
"""User token version.""" """User token version."""
return self.model.version return self.model.version
@ -154,6 +169,26 @@ def create(
return user_token return user_token
def is_valid_token(user_token: Optional[UserToken]) -> bool:
"""
Validate a token.
Token must be enabled and if it has an expiration, it must be greater
than now.
:param user_token:
:return:
"""
if user_token is None:
return False
if not user_token.enabled:
return False
if (user_token.expiration_time is not None
and user_token.expiration_time < datetime.utcnow()):
return False
return True
def delete(user_token: UserToken) -> bool: def delete(user_token: UserToken) -> bool:
""" """
Delete a user_token. Delete a user_token.
@ -177,3 +212,18 @@ def find_by_user_and_token(user: User, token: str) -> Optional[UserToken]:
:return: :return:
""" """
return UserToken.query.filter_by(user_id=user.id, token=token).first() return UserToken.query.filter_by(user_id=user.id, token=token).first()
def find_by_user(user: User, page: int, per_page: int = 20,
max_per_page: int = 100) -> Pagination:
"""
Find all tokens for a user.
:param user: The user to find tokens for
:param page: The page to request
:param per_page: The number to get per page
:param max_per_page:
:return:
"""
return UserToken.query.filter_by(
user_id=user.id).paginate(page, per_page, True, max_per_page)

15
server/corvus/service/validation_service.py

@ -4,13 +4,14 @@ from typing import Type, Dict, Callable, Any, Set, Optional, Tuple
from sqlalchemy import orm from sqlalchemy import orm
from corvus import db, errors
from corvus import errors
from corvus.db import db_model
from corvus.model import User from corvus.model import User
_changable_attribute_names: Dict[str, Set[str]] = {} _changable_attribute_names: Dict[str, Set[str]] = {}
def get_changable_attribute_names(model: Type[db.Model]) -> Set[str]:
def get_changable_attribute_names(model: db_model) -> Set[str]:
""" """
Retrieve columns from a SQLAlchemy model. Retrieve columns from a SQLAlchemy model.
@ -30,8 +31,8 @@ def get_changable_attribute_names(model: Type[db.Model]) -> Set[str]:
return model_attributes return model_attributes
def determine_change_set(original_model: Type[db.Model],
update_model: Type[db.Model],
def determine_change_set(original_model: db_model,
update_model: db_model,
model_attributes: Set[str], model_attributes: Set[str],
options: Optional[Set[str]]) -> Dict[str, Any]: options: Optional[Set[str]]) -> Dict[str, Any]:
""" """
@ -88,9 +89,9 @@ def get_change_set_value(
class BaseValidator: class BaseValidator:
"""Base Model validator.""" """Base Model validator."""
type: Type[db.Model]
type: db_model
def __init__(self, request_user: User, model: Type[db.Model]) -> None:
def __init__(self, request_user: User, model: db_model) -> None:
"""Initialize the base validator.""" """Initialize the base validator."""
self.request_user = request_user self.request_user = request_user
self._fields: Set[str] = get_changable_attribute_names(model) self._fields: Set[str] = get_changable_attribute_names(model)
@ -158,7 +159,7 @@ def register_validator(
def validate_model(request_user: User, def validate_model(request_user: User,
model_obj: db.Model,
model_obj: db_model,
change_set: Optional[Dict[str, Any]] = None) \ change_set: Optional[Dict[str, Any]] = None) \
-> ModelValidationResult: -> ModelValidationResult:
"""Lookup a Model and hand off to the validator.""" """Lookup a Model and hand off to the validator."""

5
server/dev-run.sh

@ -0,0 +1,5 @@
#!/usr/bin/env bash
FLASK_APP=corvus:corvus flask db upgrade
python manage.py user register-admin
FLASK_APP=corvus:corvus flask run

31
server/manage.py

@ -11,10 +11,14 @@ from click import Context
from corvus import corvus from corvus import corvus
from corvus.model import User from corvus.model import User
from corvus.service import user_service from corvus.service import user_service
from corvus.service.role_service import ROLE_LIST
logging.basicConfig() logging.basicConfig()
ENCODING = 'utf-8'
@click.group() @click.group()
def main(): def main():
pass pass
@ -25,6 +29,11 @@ def user_command_group():
pass pass
@click.group(name='base64')
def base64_command_group():
pass
@click.command(name='delete') @click.command(name='delete')
@click.argument('name') @click.argument('name')
def delete_user(name: str): def delete_user(name: str):
@ -46,7 +55,11 @@ def delete_user(name: str):
@click.option('--role', @click.option('--role',
default=User.ROLE_USER, default=User.ROLE_USER,
envvar='ROLE', envvar='ROLE',
help='Role to assign to the user. default=[USER]')
help='Role to assign to the user. '
+ 'default=[USER] acceptable values = ['
+ ','.join(
sorted(set(map(lambda rt: str(rt.data), ROLE_LIST))))
+ ']')
def register_user( def register_user(
name: str, name: str,
role: str, role: str,
@ -112,14 +125,24 @@ def list_users():
[click.echo(user.name) for user in all_users] [click.echo(user.name) for user in all_users]
@click.command(name='base64')
@click.command(name='encode')
@click.argument('text') @click.argument('text')
def convert_to_base64(text: str): def convert_to_base64(text: str):
print(base64.b64encode(text.encode('utf8')).decode('utf8'))
encoded_text = base64.standard_b64encode(text.encode(ENCODING)).decode(ENCODING)
logging.info('Encoded base64: \'%s\'', encoded_text)
@click.command(name='decode')
@click.argument('text')
def convert_from_base64(text: str):
decoded_text = base64.standard_b64decode(text.encode(ENCODING)).decode(ENCODING)
logging.info('Decoded base64: \'%s\'', decoded_text)
main.add_command(base64_command_group)
base64_command_group.add_command(convert_to_base64)
base64_command_group.add_command(convert_from_base64)
main.add_command(user_command_group) main.add_command(user_command_group)
main.add_command(convert_to_base64)
user_command_group.add_command(register_user) user_command_group.add_command(register_user)
user_command_group.add_command(register_admin_user) user_command_group.add_command(register_admin_user)
user_command_group.add_command(delete_user) user_command_group.add_command(delete_user)

8
server/migrations/README

@ -1 +1,7 @@
Generic single-database configuration.
# Generic single-database configuration.
## Update/Create database to latest revision
flask db upgrade
## Adding a new revision
flask db revision

1
server/mypy.ini

@ -8,4 +8,3 @@ disallow_subclassing_any = False
warn_redundant_casts = True warn_redundant_casts = True
warn_unused_ignores = True warn_unused_ignores = True
strict_optional = True strict_optional = True
strict_boolean = False

30
server/run_tests.bat

@ -0,0 +1,30 @@
SET PIPENV_VERBOSITY=-1
SET PYTHONPATH=%cd%
pipenv run python3 --version
pipenv run python3 -m pip --version
pipenv run pylint --version
pipenv run mypy --version
pipenv run coverage --version
pipenv run pytest --version
pipenv run pycodestyle --version
pipenv run pydocstyle --version
pipenv run pylint corvus
if %errorlevel% neq 0 exit /b %errorlevel%
pipenv run mypy corvus tests
if %errorlevel% neq 0 exit /b %errorlevel%
pipenv run coverage run --source corvus -m pytest
if %errorlevel% neq 0 exit /b %errorlevel%
pipenv run coverage report --fail-under=85 -m --skip-covered
if %errorlevel% neq 0 exit /b %errorlevel%
pipenv run pycodestyle corvus tests
if %errorlevel% neq 0 exit /b %errorlevel%
pipenv run pydocstyle corvus
if %errorlevel% neq 0 exit /b %errorlevel%

30
server/run_tests.sh

@ -3,20 +3,22 @@
set -e set -e
set -x set -x
# shellcheck disable=SC2034
PIPENV_VERBOSITY=-1
python3 --version
python3 -m pip --version
pipenv run python3 --version
pipenv run python3 -m pip --version
pylint --version
mypy --version
coverage --version
pytest --version
pycodestyle --version
pydocstyle --version
pipenv run pylint --version
pipenv run mypy --version
pipenv run coverage --version
pipenv run pytest --version
pipenv run pycodestyle --version
pipenv run pydocstyle --version
pylint corvus
mypy corvus tests
PYTHONPATH=$(pwd) coverage run --source corvus -m pytest
coverage report --fail-under=85 -m --skip-covered
pycodestyle corvus tests
pydocstyle corvus
pipenv run pylint corvus
pipenv run mypy corvus tests
PYTHONPATH=$(pwd) pipenv run coverage run --source corvus -m pytest
pipenv run coverage report --fail-under=85 -m --skip-covered
pipenv run pycodestyle corvus tests
pipenv run pydocstyle corvus

140
server/tests/api/test_authentication_api.py

@ -1,14 +1,21 @@
from datetime import timedelta
import rfc3339
from flask import json
from flask.testing import FlaskClient
from tests.conftest import AuthActions from tests.conftest import AuthActions
def test_login_happy_path(auth: AuthActions): def test_login_happy_path(auth: AuthActions):
result = auth.login()
with auth as result:
assert result.status_code == 200 assert result.status_code == 200
assert result.json['token'] is not None and len(result.json['token']) > 0
assert result.json[
'token'] is not None and len(result.json['token']) > 0
def test_bump_happy_path(auth: AuthActions): def test_bump_happy_path(auth: AuthActions):
auth.login()
with auth:
result = auth.bump() result = auth.bump()
assert result.status_code == 200 assert result.status_code == 200
assert (result.json['lastLoginTime'] is not None assert (result.json['lastLoginTime'] is not None
@ -20,3 +27,130 @@ def test_logout_happy_path(auth: AuthActions):
result = auth.logout() result = auth.logout()
assert result.status_code == 200 assert result.status_code == 200
assert result.json['success'] assert result.json['success']
def test_get_tokens_no_tokens(auth: AuthActions, client: FlaskClient):
auth_header = auth.get_authorization_header_basic()
result = client.get(
'/auth/token',
headers={
auth_header[0]: auth_header[1]
})
assert 404 == result.status_code
assert result.json is not None
def test_get_tokens(auth: AuthActions, client: FlaskClient):
with auth:
auth_header = auth.get_authorization_header_basic()
result = client.get(
'/auth/token',
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
assert result.json['page'] == 1
assert result.json['lastPage'] == 1
assert result.json['count'] == 1
assert result.json['totalCount'] == 1
assert result.json['items'][0]['token'] == auth.token
def test_get_nonexistant_token(auth: AuthActions, client: FlaskClient):
auth_header = auth.get_authorization_header_basic()
result = client.get(
'/auth/token/not-a-token',
headers={
auth_header[0]: auth_header[1]
})
assert 404 == result.status_code
assert result.json is not None
def test_create_get_delete_token(auth: AuthActions, client: FlaskClient):
auth_header = auth.get_authorization_header_basic()
result = client.post(
'/auth/token',
headers={
auth_header[0]: auth_header[1],
'Content-Type': 'application/json'
},
data=json.dumps({
'note': 'test note',
'enabled': False
}))
assert 200 == result.status_code
assert result.json is not None
assert result.json['token'] is not None
assert result.json['note'] == 'test note'
assert not result.json['enabled']
assert not result.json['isValid']
auth_token = result.json['token']
result = client.get(
'/auth/token/%s' % auth_token,
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
assert result.json['token'] == auth_token
result = client.delete(
'/auth/token/%s' % auth_token,
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
assert 'message' not in result.json
assert result.json['success']
def test_create_get_delete_expired_token(
auth: AuthActions, client: FlaskClient):
auth_header = auth.get_authorization_header_basic()
result = client.post(
'/auth/token',
headers={
auth_header[0]: auth_header[1],
'Content-Type': 'application/json'
},
data=json.dumps({
'note': 'test note',
'expirationTime': rfc3339.format(
rfc3339.datetime.now() - timedelta(days=1))
}))
assert 200 == result.status_code
assert result.json is not None
assert result.json['token'] is not None
assert result.json['note'] == 'test note'
assert not result.json['isValid']
auth_token = result.json['token']
result = client.get(
'/auth/token/%s' % auth_token,
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
assert result.json['token'] == auth_token
result = client.delete(
'/auth/token/%s' % auth_token,
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
assert 'message' not in result.json
assert result.json['success']
def test_delete_nonexistant_token(auth: AuthActions, client: FlaskClient):
auth_header = auth.get_authorization_header_basic()
result = client.delete(
'/auth/token/not-a-token',
headers={
auth_header[0]: auth_header[1]
})
assert 404 == result.status_code
assert result.json is not None

19
server/tests/api/test_health_api.py

@ -0,0 +1,19 @@
from datetime import datetime
from flask.testing import FlaskClient
from tests.conftest import AuthActions
def test_get_health_happy_path(auth: AuthActions, client: FlaskClient):
with auth:
auth_header = auth.get_authorization_header_token()
result = client.get(
'/health',
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
assert result.json['message'] == 'Service is healthy'
assert result.json['success']

47
server/tests/api/test_user_api.py

@ -6,9 +6,11 @@ from flask.testing import FlaskClient
from tests.conftest import AuthActions from tests.conftest import AuthActions
from corvus.service.role_service import ROLE_LIST
def test_get_users_happy_path(auth: AuthActions, client: FlaskClient): def test_get_users_happy_path(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result = client.get( result = client.get(
'/user', '/user',
@ -25,7 +27,7 @@ def test_get_users_happy_path(auth: AuthActions, client: FlaskClient):
def test_get_users_nonexistent_page(auth: AuthActions, client: FlaskClient): def test_get_users_nonexistent_page(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result = client.get( result = client.get(
'/user?page=2', '/user?page=2',
@ -36,8 +38,20 @@ def test_get_users_nonexistent_page(auth: AuthActions, client: FlaskClient):
assert result.json is not None assert result.json is not None
def test_get_users_bad_page_parameters(auth: AuthActions, client: FlaskClient):
with auth:
auth_header = auth.get_authorization_header_token()
result = client.get(
'/user?page=a',
headers={
auth_header[0]: auth_header[1]
})
assert 400 == result.status_code
assert result.json is not None
def test_get_user_happy_path(auth: AuthActions, client: FlaskClient): def test_get_user_happy_path(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result = client.get( result = client.get(
'/user/{}'.format(client.application.config['test_username']), '/user/{}'.format(client.application.config['test_username']),
@ -46,11 +60,12 @@ def test_get_user_happy_path(auth: AuthActions, client: FlaskClient):
}) })
assert 200 == result.status_code assert 200 == result.status_code
assert result.json is not None assert result.json is not None
assert result.json['name'] == client.application.config['test_username']
assert result.json['name'] == client.application.config[
'test_username']
def test_patch_user_happy_path(auth: AuthActions, client: FlaskClient): def test_patch_user_happy_path(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
last_login_time = rfc3339.format(datetime.now()) last_login_time = rfc3339.format(datetime.now())
@ -77,7 +92,7 @@ def test_patch_user_happy_path(auth: AuthActions, client: FlaskClient):
def test_register_user_happy_path(auth: AuthActions, client: FlaskClient): def test_register_user_happy_path(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result = client.post( result = client.post(
'/user', '/user',
@ -95,7 +110,7 @@ def test_register_user_happy_path(auth: AuthActions, client: FlaskClient):
def test_register_user_invalid_password( def test_register_user_invalid_password(
auth: AuthActions, client: FlaskClient): auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result = client.post( result = client.post(
'/user', '/user',
@ -113,7 +128,7 @@ def test_register_user_invalid_password(
def test_register_user_twice_failure(auth: AuthActions, client: FlaskClient): def test_register_user_twice_failure(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result1 = client.post( result1 = client.post(
'/user', '/user',
@ -142,7 +157,7 @@ def test_register_user_twice_failure(auth: AuthActions, client: FlaskClient):
def test_delete_user_happy_path(auth: AuthActions, client: FlaskClient): def test_delete_user_happy_path(auth: AuthActions, client: FlaskClient):
auth.login()
with auth:
auth_header = auth.get_authorization_header_token() auth_header = auth.get_authorization_header_token()
result1 = client.post( result1 = client.post(
'/user', '/user',
@ -164,3 +179,17 @@ def test_delete_user_happy_path(auth: AuthActions, client: FlaskClient):
assert 200 == result2.status_code assert 200 == result2.status_code
assert result2.json is not None assert result2.json is not None
assert 'message' in result2.json assert 'message' in result2.json
def test_get_roles(auth: AuthActions, client: FlaskClient):
with auth:
auth_header = auth.get_authorization_header_token()
result = client.get(
'/user/roles',
headers={
auth_header[0]: auth_header[1]
})
assert 200 == result.status_code
assert result.json is not None
for role in ROLE_LIST:
assert str(role.data) in result.json

12
server/tests/conftest.py

@ -3,7 +3,7 @@ import os
import random import random
import string import string
import tempfile import tempfile
from typing import Tuple, Any
from typing import Tuple, Any, Generator
import pytest import pytest
from flask import Flask from flask import Flask
@ -26,7 +26,7 @@ def add_test_user() -> Tuple[str, str]:
@pytest.fixture @pytest.fixture
def app() -> Flask:
def app() -> Generator[Flask, Any, Any]:
"""Create and configure a new corvus_app instance for each test.""" """Create and configure a new corvus_app instance for each test."""
# create a temporary file to isolate the database for each test # create a temporary file to isolate the database for each test
db_fd, db_path = tempfile.mkstemp(suffix='.db') db_fd, db_path = tempfile.mkstemp(suffix='.db')
@ -120,7 +120,13 @@ class AuthActions(object):
credentials = base64.b64encode( credentials = base64.b64encode(
'{}:{}'.format(self.username, self.token).encode('utf8')) \ '{}:{}'.format(self.username, self.token).encode('utf8')) \
.decode('utf8').strip() .decode('utf8').strip()
return 'Authorization', 'Token {}'.format(credentials)
return 'X-Auth-Token', '{}'.format(credentials)
def __enter__(self) -> 'AuthActions':
return self.login()
def __exit__(self, type: Any, value: Any, traceback: Any) -> None:
self.logout()
@pytest.fixture @pytest.fixture

112
server/tests/middleware/test_authentication_middleware.py

@ -10,15 +10,15 @@ middleware_module = 'corvus.middleware.authentication_middleware'
@patch(middleware_module + '.authentication_service.is_valid_password') @patch(middleware_module + '.authentication_service.is_valid_password')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_password_happy_path( def test_authenticate_with_password_happy_path(
mock_user_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_is_valid_password: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = Mock()
mock_authentication_service.return_value = True
mock_find_by_name.return_value = Mock()
mock_is_valid_password.return_value = True
assert authenticate_with_password('test', 'test') assert authenticate_with_password('test', 'test')
mock_user_service.assert_called_once()
mock_authentication_service.assert_called_once()
mock_find_by_name.assert_called_once()
mock_is_valid_password.assert_called_once()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()
@ -26,15 +26,15 @@ def test_authenticate_with_password_happy_path(
@patch(middleware_module + '.authentication_service.is_valid_password') @patch(middleware_module + '.authentication_service.is_valid_password')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_password_no_user( def test_authenticate_with_password_no_user(
mock_user_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_is_valid_password: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = None
mock_authentication_service.return_value = True
mock_find_by_name.return_value = None
mock_is_valid_password.return_value = True
assert not authenticate_with_password('test', 'test') assert not authenticate_with_password('test', 'test')
mock_user_service.assert_called_once()
mock_authentication_service.assert_not_called()
mock_find_by_name.assert_called_once()
mock_is_valid_password.assert_not_called()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()
@ -42,91 +42,91 @@ def test_authenticate_with_password_no_user(
@patch(middleware_module + '.authentication_service.is_valid_password') @patch(middleware_module + '.authentication_service.is_valid_password')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_password_invalid_password( def test_authenticate_with_password_invalid_password(
mock_user_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_is_valid_password: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = Mock()
mock_authentication_service.return_value = False
mock_find_by_name.return_value = Mock()
mock_is_valid_password.return_value = False
assert not authenticate_with_password('test', 'test') assert not authenticate_with_password('test', 'test')
mock_user_service.assert_called_once()
mock_authentication_service.assert_called_once()
mock_find_by_name.assert_called_once()
mock_is_valid_password.assert_called_once()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()
@patch(middleware_module + '.g') @patch(middleware_module + '.g')
@patch(middleware_module + '.authentication_service.is_valid_token')
@patch(middleware_module + '.user_token_service.is_valid_token')
@patch(middleware_module + '.user_token_service.find_by_user_and_token') @patch(middleware_module + '.user_token_service.find_by_user_and_token')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_token_happy_path( def test_authenticate_with_token_happy_path(
mock_user_service: MagicMock,
mock_user_token_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_find_by_user_and_token: MagicMock,
mock_is_valid_token: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = Mock()
mock_user_token_service.return_value = Mock()
mock_authentication_service.return_value = True
mock_find_by_name.return_value = Mock()
mock_find_by_user_and_token.return_value = Mock()
mock_is_valid_token.return_value = True
assert authenticate_with_token('test', 'test') assert authenticate_with_token('test', 'test')
mock_user_service.assert_called_once()
mock_user_token_service.assert_called_once()
mock_authentication_service.assert_called_once()
mock_find_by_name.assert_called_once()
mock_find_by_user_and_token.assert_called_once()
mock_is_valid_token.assert_called_once()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()
@patch(middleware_module + '.g') @patch(middleware_module + '.g')
@patch(middleware_module + '.authentication_service.is_valid_token')
@patch(middleware_module + '.user_token_service.is_valid_token')
@patch(middleware_module + '.user_token_service.find_by_user_and_token') @patch(middleware_module + '.user_token_service.find_by_user_and_token')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_token_no_user( def test_authenticate_with_token_no_user(
mock_user_service: MagicMock,
mock_user_token_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_find_by_user_and_token: MagicMock,
mock_is_valid_token: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = None
mock_find_by_name.return_value = None
assert not authenticate_with_token('test', 'test') assert not authenticate_with_token('test', 'test')
mock_user_service.assert_called_once()
mock_user_token_service.assert_not_called()
mock_authentication_service.assert_not_called()
mock_find_by_name.assert_called_once()
mock_find_by_user_and_token.assert_not_called()
mock_is_valid_token.assert_not_called()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()
@patch(middleware_module + '.g') @patch(middleware_module + '.g')
@patch(middleware_module + '.authentication_service.is_valid_token')
@patch(middleware_module + '.user_token_service.is_valid_token')
@patch(middleware_module + '.user_token_service.find_by_user_and_token') @patch(middleware_module + '.user_token_service.find_by_user_and_token')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_token_no_user_token( def test_authenticate_with_token_no_user_token(
mock_user_service: MagicMock,
mock_user_token_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_find_by_user_and_token: MagicMock,
mock_is_valid_token: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = Mock()
mock_user_token_service.return_value = None
mock_authentication_service.return_value = False
mock_find_by_name.return_value = Mock()
mock_find_by_user_and_token.return_value = None
mock_is_valid_token.return_value = False
assert not authenticate_with_token('test', 'test') assert not authenticate_with_token('test', 'test')
mock_user_service.assert_called_once()
mock_user_token_service.assert_called_once()
mock_authentication_service.assert_called_once()
mock_find_by_name.assert_called_once()
mock_find_by_user_and_token.assert_called_once()
mock_is_valid_token.assert_called_once()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()
@patch(middleware_module + '.g') @patch(middleware_module + '.g')
@patch(middleware_module + '.authentication_service.is_valid_token')
@patch(middleware_module + '.user_token_service.is_valid_token')
@patch(middleware_module + '.user_token_service.find_by_user_and_token') @patch(middleware_module + '.user_token_service.find_by_user_and_token')
@patch(middleware_module + '.user_service.find_by_name') @patch(middleware_module + '.user_service.find_by_name')
def test_authenticate_with_token_invalid_token( def test_authenticate_with_token_invalid_token(
mock_user_service: MagicMock,
mock_user_token_service: MagicMock,
mock_authentication_service: MagicMock,
mock_find_by_name: MagicMock,
mock_find_by_user_and_token: MagicMock,
mock_is_valid_token: MagicMock,
mock_g: MagicMock): mock_g: MagicMock):
mock_g.user = Mock() mock_g.user = Mock()
mock_user_service.return_value = Mock()
mock_user_token_service.return_value = Mock()
mock_authentication_service.return_value = False
mock_find_by_name.return_value = Mock()
mock_find_by_user_and_token.return_value = Mock()
mock_is_valid_token.return_value = False
assert not authenticate_with_token('test', 'test') assert not authenticate_with_token('test', 'test')
mock_user_service.assert_called_once()
mock_user_token_service.assert_called_once()
mock_authentication_service.assert_called_once()
mock_find_by_name.assert_called_once()
mock_find_by_user_and_token.assert_called_once()
mock_is_valid_token.assert_called_once()
mock_g.user.assert_not_called() mock_g.user.assert_not_called()

38
server/tests/service/test_authentication_service.py

@ -0,0 +1,38 @@
import pytest
from corvus import errors
from corvus.service import authentication_service
def test_validate_password_strength_good_password():
proposed_good_password = 'AazZ1001'
assert proposed_good_password == authentication_service\
.validate_password_strength(proposed_good_password)
def test_validate_password_strength_too_short():
proposed_good_password = 'AazZ100'
with pytest.raises(errors.ValidationError) as error_info:
authentication_service.validate_password_strength(
proposed_good_password)
def test_validate_password_strength_missing_uppercase():
proposed_good_password = 'aazz1001'
with pytest.raises(errors.ValidationError) as error_info:
authentication_service.validate_password_strength(
proposed_good_password)
def test_validate_password_strength_missing_lowercase():
proposed_good_password = 'AAZZ1001'
with pytest.raises(errors.ValidationError) as error_info:
authentication_service.validate_password_strength(
proposed_good_password)
def test_validate_password_strength_missing_numbers():
proposed_good_password = 'AAZZZZAA'
with pytest.raises(errors.ValidationError) as error_info:
authentication_service.validate_password_strength(
proposed_good_password)

7
server/tests/service/test_patch_service.py

@ -7,12 +7,8 @@ from corvus import errors
from corvus.model import UserToken, User from corvus.model import UserToken, User
from corvus.service import patch_service, role_service from corvus.service import patch_service, role_service
service_module = 'corvus.service.patch_service'
@patch(service_module + '.db.session.commit')
def test_patch_models(
mock_db_session_commit: MagicMock):
def test_patch_models():
request_user = User() request_user = User()
request_user.role = role_service.Role.ADMIN request_user.role = role_service.Role.ADMIN
@ -29,7 +25,6 @@ def test_patch_models(
patched_user = patch_service.patch(request_user, user, user_patch) patched_user = patch_service.patch(request_user, user, user_patch)
assert patched_user.version > 1 assert patched_user.version > 1
assert patched_user.last_login_time == user_patch.last_login_time assert patched_user.last_login_time == user_patch.last_login_time
mock_db_session_commit.assert_called_once()
def test_patch_of_different_types(): def test_patch_of_different_types():

6
server/tests/service/test_role_service.py

@ -3,16 +3,18 @@ from corvus.service.role_service import ROLES, Role
def test_role_tree_find_roles_in_hierarchy(): def test_role_tree_find_roles_in_hierarchy():
roles = ROLES.find_roles_in_hierarchy(Role.USER) roles = ROLES.find_roles_in_hierarchy(Role.USER)
assert len(roles) == 4
assert len(roles) == 5
assert Role.USER in roles assert Role.USER in roles
assert Role.MODERATOR in roles assert Role.MODERATOR in roles
assert Role.AUDITOR in roles assert Role.AUDITOR in roles
assert Role.ADMIN in roles assert Role.ADMIN in roles
assert Role.OWNER in roles
roles = ROLES.find_roles_in_hierarchy(Role.AUDITOR) roles = ROLES.find_roles_in_hierarchy(Role.AUDITOR)
assert len(roles) == 2
assert len(roles) == 3
assert Role.AUDITOR in roles assert Role.AUDITOR in roles
assert Role.ADMIN in roles assert Role.ADMIN in roles
assert Role.OWNER in roles
def test_role_tree_find_children_roles(): def test_role_tree_find_children_roles():

Loading…
Cancel
Save