diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..0da6355 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +--- +name: Lint Code Base + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Lint Code Base + runs-on: ubuntu-20.04 + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Lint Code Base + uses: github/super-linter@v3 + env: + VALIDATE_ALL_CODEBASE: false + IGNORE_GITIGNORED_FILES: true + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b320e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Scott Rubin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..945b8b5 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Djangogoboot-template + +This is a special Django project template that is used by Djangogoboot. Djangogoboot a small Python CLI program that will use this template to start a new Django project from scratch with a complete single-instance CI/CD production stack powered by GitHub Actions. + +See the Djangogoboot project at [https://github.com/Apreche/djangogoboot/](https://github.com/Apreche/djangogoboot/) for more information. + +## Using without Djangogoboot + +It is possible to use this template on its own without Djangogoboot, but it will be quiet tedious. If that's really a road you would liek to go down, the source code of Djangogoboot is effectively the documentation for this process. diff --git a/README.md-tpl b/README.md-tpl new file mode 100644 index 0000000..2162d0e --- /dev/null +++ b/README.md-tpl @@ -0,0 +1,3 @@ +# {{ project_name }} + +[![GitHub Super-Linter](https://github.com/{% templatetag openvariable %} full_repo_name {% templatetag closevariable %}/workflows/Lint%20Code%20Base/badge.svg)](https://github.com/marketplace/actions/super-linter) diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..244e31e --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,7 @@ +[defaults] +remote_tmp = /tmp/ansible +deprecation_warnings = False +interpreter_python = /usr/bin/python3 + +[ssh_connection] +pipelining = True diff --git a/ansible/deploy.yml b/ansible/deploy.yml new file mode 100644 index 0000000..bc5508d --- /dev/null +++ b/ansible/deploy.yml @@ -0,0 +1,5 @@ +--- +- hosts: all + roles: + - role: full_stack +... diff --git a/ansible/gitignore b/ansible/gitignore new file mode 100644 index 0000000..a8b42eb --- /dev/null +++ b/ansible/gitignore @@ -0,0 +1 @@ +*.retry diff --git a/ansible/group_vars/all/vault.yml b/ansible/group_vars/all/vault.yml new file mode 100644 index 0000000..e69de29 diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..acfcd72 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - name: community.general + version: 3.0.0 +... diff --git a/ansible/roles/cache/defaults/main.yml b/ansible/roles/cache/defaults/main.yml new file mode 100644 index 0000000..8932879 --- /dev/null +++ b/ansible/roles/cache/defaults/main.yml @@ -0,0 +1,8 @@ +--- +memcached_runtime_dir: "{{ runtime_dir }}/memcached" +memcached_socket: "{{ memcached_runtime_dir }}/memcached.sock" +memcached_pid_file: "{{ memcached_runtime_dir }}/memcached.pid" +memcached_config_file: "{{ config_dir }}/memcached.conf" +memcached_log_file: "{{ log_dir }}/memcached.log" +memcached_memory: 64 +... diff --git a/ansible/roles/cache/meta/main.yml b/ansible/roles/cache/meta/main.yml new file mode 100644 index 0000000..38a3830 --- /dev/null +++ b/ansible/roles/cache/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: common +... diff --git a/ansible/roles/cache/tasks/install_memcached.yml b/ansible/roles/cache/tasks/install_memcached.yml new file mode 100644 index 0000000..e0ad929 --- /dev/null +++ b/ansible/roles/cache/tasks/install_memcached.yml @@ -0,0 +1,23 @@ +--- +- name: Install memcached + ansible.builtin.apt: + name: memcached + state: present + update_cache: yes + become: yes + +- name: Install memcached configuration file + ansible.builtin.template: + src: templates/memcached.conf.j2 + dest: "{{ memcached_config_file }}" + owner: root + group: root + mode: '0644' + become: yes + +- name: Ensure memcached is started + ansible.builtin.service: + name: memcached + state: started + become: yes +... diff --git a/ansible/roles/cache/tasks/main.yml b/ansible/roles/cache/tasks/main.yml new file mode 100644 index 0000000..505ee75 --- /dev/null +++ b/ansible/roles/cache/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- include_tasks: install_memcached.yml +... diff --git a/ansible/roles/cache/templates/memcached.conf.j2 b/ansible/roles/cache/templates/memcached.conf.j2 new file mode 100644 index 0000000..479ba39 --- /dev/null +++ b/ansible/roles/cache/templates/memcached.conf.j2 @@ -0,0 +1,13 @@ +-d + +-m {{ memcached_memory }} + +-u memcache + +-a 777 + +-s {{ memcached_socket | default('/run/memcached/memcached.sock') }} + +-P {{ memcached_pid_file | default('/run/memcached/memcached.pid') }} + +logfile {{ memcached_log_file | default('/var/log/memcached.log') }} diff --git a/ansible/roles/common/defaults/main/project.yml b/ansible/roles/common/defaults/main/project.yml new file mode 100644 index 0000000..c4478e1 --- /dev/null +++ b/ansible/roles/common/defaults/main/project.yml @@ -0,0 +1,34 @@ +--- +project_name: "{{ lookup('env', 'GITHUB_REPOSITORY').split('/')[1] | lower }}" +owner: "{{ lookup('env', 'GITHUB_REPOSITORY').split('/')[0] | lower }}" +email_address: "{{ lookup('env', 'EMAIL_ADDRESS') }}" +web_domain: "{{ lookup('env', 'WEB_DOMAIN') | default(inventory_hostname, true) }}" + +git_repo: "git@github.com:{{ lookup('env', 'GITHUB_REPOSITORY') }}.git" +git_version: "{{ lookup('env', 'GITHUB_REF') | default(lookup('env', 'GITHUB_SHA'), True) }}" + + +base_dir: "{{ ansible_env.HOME }}/projects" +config_dir: "/etc" +runtime_dir: "/run" +temp_dir: "/tmp" +log_dir: "/var/log" +state_dir: "/var/lib" +www_dir: "/var/www" + +ansible_temp_dir: "{{ temp_dir }}/ansible" + +project_source_dir: "{{ base_dir }}/{{ project_name }}" +project_config_dir: "{{ config_dir }}/{{ project_name }}" +project_runtime_dir: "{{ runtime_dir }}/{{ project_name }}" +project_temp_dir: "{{ temp_dir }}/{{ project_name }}" +project_log_dir: "{{ log_dir }}/{{ project_name }}" +project_state_dir: "{{ state_dir }}/{{ project_name }}" +project_www_dir: "{{ www_dir }}/{{ project_name }}" + +ssh_dir: "{{ ansible_env.HOME }}/.ssh" +deploy_key: "{{ lookup('env', 'DEPLOY_SSH_PRIVATE_KEY') }}" +deploy_key_path: "{{ ssh_dir }}/{{ project_name }}_deploykey" + +systemd_config_dir: "{{ config_dir }}/systemd/system" +... diff --git a/ansible/roles/common/meta/main.yml b/ansible/roles/common/meta/main.yml new file mode 100644 index 0000000..91da2a7 --- /dev/null +++ b/ansible/roles/common/meta/main.yml @@ -0,0 +1,2 @@ +--- +... diff --git a/ansible/roles/common/tasks/deploy_key.yml b/ansible/roles/common/tasks/deploy_key.yml new file mode 100644 index 0000000..34d0b77 --- /dev/null +++ b/ansible/roles/common/tasks/deploy_key.yml @@ -0,0 +1,8 @@ +--- +- name: Install Deploy Key + ansible.builtin.template: + src: deploy_key.j2 + force: yes + dest: "{{ deploy_key_path }}" + mode: '0600' +... diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000..5a49f44 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,5 @@ +--- +- include_tasks: register_vars.yml +- include_tasks: make_dirs.yml +- include_tasks: deploy_key.yml +... diff --git a/ansible/roles/common/tasks/make_dirs.yml b/ansible/roles/common/tasks/make_dirs.yml new file mode 100644 index 0000000..3ca1ea7 --- /dev/null +++ b/ansible/roles/common/tasks/make_dirs.yml @@ -0,0 +1,63 @@ +--- +- name: Create ansible remote temp dir + ansible.builtin.file: + path: "{{ ansible_temp_dir }}" + state: directory + mode: '0777' + +- name: Ensure project source directory exists + ansible.builtin.file: + path: "{{ project_source_dir }}" + state: directory + mode: '0755' + +- name: Ensure project config directory exists + ansible.builtin.file: + path: "{{ project_config_dir }}" + state: directory + mode: '0755' + become: yes + +- name: Ensure project runtime directory exists + ansible.builtin.file: + path: "{{ project_runtime_dir }}" + state: directory + mode: '0755' + become: yes + +- name: Ensure project temp directory exists + ansible.builtin.file: + path: "{{ project_temp_dir }}" + state: directory + mode: '0755' + become: yes + +- name: Ensure project log directory exists + ansible.builtin.file: + path: "{{ project_log_dir }}" + state: directory + mode: '0775' + become: yes + +- name: Ensure project state directory exists + ansible.builtin.file: + path: "{{ project_state_dir }}" + state: directory + mode: '0775' + become: yes + +- name: Ensure project www directory exists + ansible.builtin.file: + path: "{{ project_www_dir }}" + state: directory + owner: root + group: www-data + mode: '0755' + become: yes + +- name: Ensure SSH directory exists + ansible.builtin.file: + path: "{{ ssh_dir }}" + state: directory + mode: '0755' +... diff --git a/ansible/roles/common/tasks/register_vars.yml b/ansible/roles/common/tasks/register_vars.yml new file mode 100644 index 0000000..61410b0 --- /dev/null +++ b/ansible/roles/common/tasks/register_vars.yml @@ -0,0 +1,11 @@ +--- +- name: Get Ubuntu release name + ansible.builtin.command: lsb_release -cs + register: ubuntu_release_string + changed_when: false + check_mode: false + +- name: Register common vars + ansible.builtin.set_fact: + ubuntu_release: "{{ ubuntu_release_string.stdout }}" +... diff --git a/ansible/roles/common/templates/deploy_key.j2 b/ansible/roles/common/templates/deploy_key.j2 new file mode 100644 index 0000000..b5a59a2 --- /dev/null +++ b/ansible/roles/common/templates/deploy_key.j2 @@ -0,0 +1 @@ +{{ deploy_key }} diff --git a/ansible/roles/db/defaults/main.yml b/ansible/roles/db/defaults/main.yml new file mode 100644 index 0000000..d976821 --- /dev/null +++ b/ansible/roles/db/defaults/main.yml @@ -0,0 +1,7 @@ +--- +postgresql_db_name: "{{ project_name }}" +postgresql_user: "{{ project_name }}" +postgresql_password: "{{ project_name }}" +postgresql_host: "localhost" +postgresql_port: "5432" +... diff --git a/ansible/roles/db/meta/main.yml b/ansible/roles/db/meta/main.yml new file mode 100644 index 0000000..38a3830 --- /dev/null +++ b/ansible/roles/db/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: common +... diff --git a/ansible/roles/db/tasks/configure_postgres.yml b/ansible/roles/db/tasks/configure_postgres.yml new file mode 100644 index 0000000..ec79859 --- /dev/null +++ b/ansible/roles/db/tasks/configure_postgres.yml @@ -0,0 +1,32 @@ +--- +- name: Install Ansible PostgreSQL dependencies + ansible.builtin.apt: + pkg: + - libpq-dev + - python3-psycopg2 + become: yes + +- name: Create PostgreSQL database + community.general.postgresql_db: + name: "{{ postgresql_db_name }}" + become: yes + become_user: postgres + +- name: Create PostgreSQL database user + community.general.postgresql_user: + db: "{{ postgresql_db_name }}" + name: "{{ postgresql_user }}" + password: "{{ postgresql_password }}" + priv: ALL + role_attr_flags: SUPERUSER,CREATEDB + expires: infinity + become: yes + become_user: postgres + +- name: Set PostgreSQL database owner + community.general.postgresql_db: + name: "{{ postgresql_db_name }}" + owner: "{{ postgresql_password }}" + become: yes + become_user: postgres +... diff --git a/ansible/roles/db/tasks/install_postgres.yml b/ansible/roles/db/tasks/install_postgres.yml new file mode 100644 index 0000000..d7e3206 --- /dev/null +++ b/ansible/roles/db/tasks/install_postgres.yml @@ -0,0 +1,27 @@ +--- +# Thank you to this URL +# http://postgresql.freeideas.cz/ansible-simple-playbook-installing-postgresql-ubuntu-debian/ +- name: Add PostgreSQL repository key + ansible.builtin.apt_key: + url: https://www.postgresql.org/media/keys/ACCC4CF8.asc + state: present + become: yes + +- name: Add PostgreSQL repository + ansible.builtin.apt_repository: + repo: "deb [arch=amd64] http://apt.postgresql.org/pub/repos/apt {{ ubuntu_release }}-pgdg main" + state: present + become: yes + +- name: Install PostgreSQL + ansible.builtin.apt: + name: postgresql + update_cache: yes + become: yes + +- name: Ensure PostgreSQL is started + ansible.builtin.service: + name: postgresql + state: started + become: yes +... diff --git a/ansible/roles/db/tasks/main.yml b/ansible/roles/db/tasks/main.yml new file mode 100644 index 0000000..d469cab --- /dev/null +++ b/ansible/roles/db/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- include_tasks: install_postgres.yml +- include_tasks: configure_postgres.yml +... diff --git a/ansible/roles/full_stack/meta/main.yml b/ansible/roles/full_stack/meta/main.yml new file mode 100644 index 0000000..6434257 --- /dev/null +++ b/ansible/roles/full_stack/meta/main.yml @@ -0,0 +1,12 @@ +--- +dependencies: + - role: common + - role: postfix + - role: cache + - role: queue + - role: db + - role: python + - role: migrator + - role: worker + - role: web +... diff --git a/ansible/roles/migrator/meta/main.yml b/ansible/roles/migrator/meta/main.yml new file mode 100644 index 0000000..619b61f --- /dev/null +++ b/ansible/roles/migrator/meta/main.yml @@ -0,0 +1,6 @@ +--- +dependencies: + - role: common + - role: db + - role: python +... diff --git a/ansible/roles/migrator/tasks/main.yml b/ansible/roles/migrator/tasks/main.yml new file mode 100644 index 0000000..9f0ece5 --- /dev/null +++ b/ansible/roles/migrator/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- include_tasks: migrate_database.yml +... diff --git a/ansible/roles/migrator/tasks/migrate_database.yml b/ansible/roles/migrator/tasks/migrate_database.yml new file mode 100644 index 0000000..fa5a1bd --- /dev/null +++ b/ansible/roles/migrator/tasks/migrate_database.yml @@ -0,0 +1,6 @@ +--- +- name: Run database migrations + ansible.builtin.command: + cmd: "{{ virtualenv_python_bin }} {{ project_source_dir }}/manage.py migrate --noinput" + environment: "{{ env_vars }}" +... diff --git a/ansible/roles/postfix/tasks/local_only.yml b/ansible/roles/postfix/tasks/local_only.yml new file mode 100644 index 0000000..37827d2 --- /dev/null +++ b/ansible/roles/postfix/tasks/local_only.yml @@ -0,0 +1,23 @@ +--- +- name: Set postfix mailer type to local only + ansible.builtin.debconf: + name: postfix + question: postfix/main_mailer_type + vtype: string + value: "Local only" + become: yes + +- name: Set postfix mailname to localhost + ansible.builtin.debconf: + name: postfix + question: postfix/mailname + vtype: string + value: "localhost" + become: yes + +- name: Install postfix + ansible.builtin.apt: + pkg: + - postfix + become: yes +... diff --git a/ansible/roles/postfix/tasks/main.yml b/ansible/roles/postfix/tasks/main.yml new file mode 100644 index 0000000..5dd3520 --- /dev/null +++ b/ansible/roles/postfix/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- include_tasks: local_only.yml +... diff --git a/ansible/roles/python/defaults/main/environment.yml b/ansible/roles/python/defaults/main/environment.yml new file mode 100644 index 0000000..200cd0d --- /dev/null +++ b/ansible/roles/python/defaults/main/environment.yml @@ -0,0 +1,22 @@ +--- +python_environment: + vars: + debug: False + secret_key: "{{ vault_secret_key }}" + hosts: "{{ web_domain }}" + db_name: "{{ postgresql_db_name }}" + db_user: "{{ postgresql_user }}" + db_password: "{{ postgresql_password }}" + db_host: "{{ postgresql_host }}" + db_port: "{{ postgresql_port }}" + memcached_socket: "{{ memcached_socket }}" + static_url: "/static/" + static_root: "{{ project_www_dir }}/static/" + media_url: "/media/" + media_root: "{{ project_www_dir }}/media/" + celery_user: "{{ rabbitmq_user }}" + celery_password: "{{ rabbitmq_password }}" + celery_host: "{{ rabbitmq_host }}" + celery_port: "{{ rabbitmq_port }}" + celery_vhost: "{{ rabbitmq_vhost }}" +... diff --git a/ansible/roles/python/defaults/main/virtualenv.yml b/ansible/roles/python/defaults/main/virtualenv.yml new file mode 100644 index 0000000..aa6ca61 --- /dev/null +++ b/ansible/roles/python/defaults/main/virtualenv.yml @@ -0,0 +1,6 @@ +--- +virtualenv_base_dir: "{{ ansible_env.HOME }}/.virtualenvs" +virtualenv_project_dir: "{{ virtualenv_base_dir }}/{{ project_name }}" +virtualenv_bin_dir: "{{ virtualenv_project_dir }}/bin" +virtualenv_python_bin: "{{ virtualenv_bin_dir }}/python" +... diff --git a/ansible/roles/python/meta/main.yml b/ansible/roles/python/meta/main.yml new file mode 100644 index 0000000..38a3830 --- /dev/null +++ b/ansible/roles/python/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: common +... diff --git a/ansible/roles/python/tasks/environment.yml b/ansible/roles/python/tasks/environment.yml new file mode 100644 index 0000000..26aa1a0 --- /dev/null +++ b/ansible/roles/python/tasks/environment.yml @@ -0,0 +1,7 @@ +--- +- name: Set python environment variables + ansible.builtin.set_fact: + env_vars: "{{ env_vars|default({}) | combine( { project_name | upper + '_' + item.key | upper: item.value } ) }}" + loop: "{{ python_environment.vars | dict2items }}" + no_log: True +... diff --git a/ansible/roles/python/tasks/install_python.yml b/ansible/roles/python/tasks/install_python.yml new file mode 100644 index 0000000..4d57dae --- /dev/null +++ b/ansible/roles/python/tasks/install_python.yml @@ -0,0 +1,86 @@ +--- +- name: Add deadsnakes ppa + ansible.builtin.apt_repository: + repo: 'ppa:deadsnakes/ppa' + become: yes + +- name: Install Python3 packages + ansible.builtin.apt: + pkg: + - python3 + - python3-dev + - python3-distutils + - python3-testresources + - python3-pip + - python3-setuptools + - python3-venv + become: yes + +- name: Set python3 as default + community.general.alternatives: + name: python + link: /usr/bin/python + path: /usr/bin/python3 + priority: 1 + become: yes + +- name: Install system dependencies for Python modules + ansible.builtin.apt: + pkg: + - build-essential + - libfreetype6-dev + - libjpeg-dev + - liblcms2-dev + - libmemcached-dev + - libpq-dev + - libtiff-dev + - libwebp-dev + - tcl-dev + - tk-dev + - python3-tk + - zlib1g-dev + become: yes + +- name: Upgrade pip using pip + ansible.builtin.pip: + name: pip + state: latest + become: yes + +- name: Install virtualenv + ansible.builtin.pip: + name: virtualenv + state: latest + become: yes + +- name: Upgrade virtualenv embed wheels + ansible.builtin.command: + cmd: "virtualenv --upgrade-embed-wheels" + become: yes + +- name: Install poetry + ansible.builtin.pip: + name: poetry + state: latest + +- name: Set poetry bin and requirements.txt paths + ansible.builtin.set_fact: + poetry_bin: "{{ ansible_env.HOME }}/.local/bin/poetry" + requirements_file: "{{ project_source_dir }}/requirements.txt" + +- name: Generate requirements.txt + ansible.builtin.command: + chdir: "{{ project_source_dir }}" + cmd: "{{ poetry_bin }} export -f requirements.txt --output {{ requirements_file }}" + +- name: Install project Python requirements + ansible.builtin.pip: + requirements: "{{ requirements_file }}" + virtualenv: "{{ virtualenv_project_dir }}" + notify: "restart web application server" + +- name: Remove requirements.txt + ansible.builtin.file: + path: "{{ requirements_file }}" + state: absent +... diff --git a/ansible/roles/python/tasks/main.yml b/ansible/roles/python/tasks/main.yml new file mode 100644 index 0000000..74fe859 --- /dev/null +++ b/ansible/roles/python/tasks/main.yml @@ -0,0 +1,5 @@ +--- +- include_tasks: environment.yml +- include_tasks: update_code.yml +- include_tasks: install_python.yml +... diff --git a/ansible/roles/python/tasks/update_code.yml b/ansible/roles/python/tasks/update_code.yml new file mode 100644 index 0000000..55b064e --- /dev/null +++ b/ansible/roles/python/tasks/update_code.yml @@ -0,0 +1,10 @@ +--- +- name: Checkout code + ansible.builtin.git: + repo: "{{ git_repo }}" + dest: "{{ project_source_dir }}" + version: "{{ git_version }}" + key_file: "{{ deploy_key_path }}" + accept_hostkey: yes + umask: '0022' +... diff --git a/ansible/roles/queue/defaults/main.yml b/ansible/roles/queue/defaults/main.yml new file mode 100644 index 0000000..86b04cd --- /dev/null +++ b/ansible/roles/queue/defaults/main.yml @@ -0,0 +1,7 @@ +--- +rabbitmq_vhost: "{{ project_name }}/" +rabbitmq_user: "{{ project_name }}" +rabbitmq_password: "{{ project_name }}" +rabbitmq_host: "localhost" +rabbitmq_port: "5672" +... diff --git a/ansible/roles/queue/meta/main.yml b/ansible/roles/queue/meta/main.yml new file mode 100644 index 0000000..38a3830 --- /dev/null +++ b/ansible/roles/queue/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - role: common +... diff --git a/ansible/roles/queue/tasks/install_rabbitmq.yml b/ansible/roles/queue/tasks/install_rabbitmq.yml new file mode 100644 index 0000000..30e0dda --- /dev/null +++ b/ansible/roles/queue/tasks/install_rabbitmq.yml @@ -0,0 +1,31 @@ +--- +- name: Install rabbitmq + ansible.builtin.apt: + name: rabbitmq-server + state: present + update_cache: yes + become: yes + +- name: Ensure rabbitmq is started + ansible.builtin.service: + name: rabbitmq-server + state: started + become: yes + +- name: Create rabbitmq vhost + community.rabbitmq.rabbitmq_vhost: + name: "{{ rabbitmq_vhost }}" + state: present + become: yes + +- name: Create rabbitmq user + community.rabbitmq.rabbitmq_user: + name: "{{ rabbitmq_user }}" + password: "{{ rabbitmq_password }}" + vhost: "{{ rabbitmq_vhost }}" + configure_priv: .* + read_priv: .* + write_priv: .* + state: present + become: yes +... diff --git a/ansible/roles/queue/tasks/main.yml b/ansible/roles/queue/tasks/main.yml new file mode 100644 index 0000000..a60f3b2 --- /dev/null +++ b/ansible/roles/queue/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- include_tasks: install_rabbitmq.yml +... diff --git a/ansible/roles/web/defaults/main/certbot.yml b/ansible/roles/web/defaults/main/certbot.yml new file mode 100644 index 0000000..fd84786 --- /dev/null +++ b/ansible/roles/web/defaults/main/certbot.yml @@ -0,0 +1,4 @@ +--- +certbot_conf_dir: "{{ config_dir }}/letsencrypt" +certbot_key_dir: "{{ certbot_conf_dir }}/live/{{ web_domain }}" +... diff --git a/ansible/roles/web/defaults/main/gunicorn.yml b/ansible/roles/web/defaults/main/gunicorn.yml new file mode 100644 index 0000000..614fc53 --- /dev/null +++ b/ansible/roles/web/defaults/main/gunicorn.yml @@ -0,0 +1,12 @@ +--- +gunicorn_service_name: "{{ project_name }}_gunicorn" +gunicorn_config_dir: "{{ project_config_dir }}/gunicorn" +gunicorn_environment_path: "{{ gunicorn_config_dir }}/environment" +gunicorn_config_path: "{{ gunicorn_config_dir }}/config.py" +gunicorn_error_log: "{{ project_log_dir }}/gunicorn/gunicorn_error.log" +gunicorn_pid_file: "{{ project_runtime_dir }}/gunicorn/gunicorn.pid" +gunicorn_socket: "{{ project_runtime_dir }}/gunicorn.sock" +gunicorn_capture_output: True +gunicorn_timeout: 30 +gunicorn_keepalive: 10 +... diff --git a/ansible/roles/web/defaults/main/nginx.yml b/ansible/roles/web/defaults/main/nginx.yml new file mode 100644 index 0000000..5cf0441 --- /dev/null +++ b/ansible/roles/web/defaults/main/nginx.yml @@ -0,0 +1,11 @@ +--- +nginx_config_dir: "{{ config_dir }}/nginx" +nginx_config_available_dir: "{{ nginx_config_dir }}/sites-available" +nginx_config_enabled_dir: "{{ nginx_config_dir }}/sites-enabled" +nginx_config_file: "{{ nginx_config_available_dir }}/{{ project_name }}" +nginx_symlink_path: "{{ nginx_config_enabled_dir }}/{{ project_name }}" +nginx_log_dir: "{{ log_dir }}/nginx" +nginx_access_log: "{{ nginx_log_dir }}/{{ project_name }}_access.log" +nginx_error_log: "{{ nginx_log_dir }}/{{ project_name }}_error.log" +nginx_www_root_dir: "{{ project_www_dir }}" +... diff --git a/ansible/roles/web/handlers/main.yml b/ansible/roles/web/handlers/main.yml new file mode 100644 index 0000000..db9d88f --- /dev/null +++ b/ansible/roles/web/handlers/main.yml @@ -0,0 +1,8 @@ +--- +- name: Restart gunicorn + ansible.builtin.service: + name: "{{ gunicorn_service_name }}" + state: restarted + listen: "restart web application server" + become: yes +... diff --git a/ansible/roles/web/meta/main.yml b/ansible/roles/web/meta/main.yml new file mode 100644 index 0000000..5cc4aff --- /dev/null +++ b/ansible/roles/web/meta/main.yml @@ -0,0 +1,9 @@ +--- +dependencies: + - role: common + - role: cache + - role: db + - role: queue + - role: python + - role: migrator +... diff --git a/ansible/roles/web/tasks/certbot.yml b/ansible/roles/web/tasks/certbot.yml new file mode 100644 index 0000000..985fa4c --- /dev/null +++ b/ansible/roles/web/tasks/certbot.yml @@ -0,0 +1,38 @@ +--- +- name: Install snap core + community.general.snap: + name: + - core + become: yes + +- name: Install certbot + community.general.snap: + name: + - certbot + classic: yes + become: yes + +- name: Create certbot symlink + ansible.builtin.file: + src: /snap/bin/certbot + dest: /usr/bin/certbot + owner: root + group: root + state: link + become: yes + +- name: Run certbot + ansible.builtin.command: + argv: + - "certbot" + - "certonly" + - "--nginx" + - "--non-interactive" + - "--keep" + - "--agree-tos" + - "--no-eff-email" + - "-m {{ email_address }}" + - "-d {{ web_domain }}" + creates: "{{ certbot_key_dir }}/fullchain.pem" + become: yes +... diff --git a/ansible/roles/web/tasks/gunicorn.yml b/ansible/roles/web/tasks/gunicorn.yml new file mode 100644 index 0000000..1c8d437 --- /dev/null +++ b/ansible/roles/web/tasks/gunicorn.yml @@ -0,0 +1,63 @@ +--- +- name: Ensuer gunicorn config directory exists + ansible.builtin.file: + path: "{{ gunicorn_config_dir }}" + state: directory + mode: '0755' + become: yes + +- name: Install gunicorn environment file + ansible.builtin.template: + src: templates/gunicorn_environment.j2 + dest: "{{ gunicorn_environment_path }}" + owner: root + group: www-data + mode: '0640' + become: yes + +- name: Install gunicorn config file + ansible.builtin.template: + src: templates/gunicorn_config.j2 + dest: "{{ gunicorn_config_path }}" + owner: root + group: www-data + mode: '0644' + become: yes + notify: "restart web application server" + +- name: Install gunicorn systemd socket config + ansible.builtin.template: + src: templates/systemd_gunicorn_socket.j2 + dest: "{{ systemd_config_dir }}/{{ gunicorn_service_name }}.socket" + owner: root + group: root + mode: '0644' + become: yes + notify: "restart web application server" + +- name: Install gunicorn systemd service config + ansible.builtin.template: + src: templates/systemd_gunicorn.j2 + dest: "{{ systemd_config_dir }}/{{ gunicorn_service_name }}.service" + owner: root + group: root + mode: '0644' + become: yes + notify: "restart web application server" + +- name: Enable and start gunicorn socket + ansible.builtin.systemd: + name: "{{ gunicorn_service_name }}.socket" + enabled: yes + state: started + daemon_reload: yes + become: yes + +- name: Enable and start gunicorn + ansible.builtin.systemd: + name: "{{ gunicorn_service_name }}.service" + enabled: yes + state: started + daemon_reload: yes + become: yes +... diff --git a/ansible/roles/web/tasks/main.yml b/ansible/roles/web/tasks/main.yml new file mode 100644 index 0000000..00759f2 --- /dev/null +++ b/ansible/roles/web/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- include_tasks: media.yml +- include_tasks: static.yml +- include_tasks: gunicorn.yml +- include_tasks: nginx.yml +... diff --git a/ansible/roles/web/tasks/media.yml b/ansible/roles/web/tasks/media.yml new file mode 100644 index 0000000..67023ee --- /dev/null +++ b/ansible/roles/web/tasks/media.yml @@ -0,0 +1,10 @@ +--- +- name: Ensure media_root directory exists + ansible.builtin.file: + path: "{{ python_environment.vars.media_root }}" + state: directory + owner: root + group: www-data + mode: '0775' + become: yes +... diff --git a/ansible/roles/web/tasks/nginx.yml b/ansible/roles/web/tasks/nginx.yml new file mode 100644 index 0000000..b1925f3 --- /dev/null +++ b/ansible/roles/web/tasks/nginx.yml @@ -0,0 +1,46 @@ +--- +- name: Install NGINX + ansible.builtin.apt: + name: "nginx" + state: present + update_cache: yes + become: yes + +- name: Ensure NGINX is started + ansible.builtin.service: + name: nginx + state: started + become: yes + +- include_tasks: certbot.yml + +- name: Install NGINX configuration file + ansible.builtin.template: + src: templates/nginx.j2 + dest: "{{ nginx_config_file }}" + owner: root + group: root + mode: '0644' + become: yes + +- name: Remove NGINX default configuration symlink + ansible.builtin.file: + path: "{{ nginx_config_enabled_dir }}/default" + state: absent + become: yes + +- name: Create NGINX app configuration symlink + ansible.builtin.file: + src: "{{ nginx_config_file }}" + dest: "{{ nginx_symlink_path }}" + owner: root + group: root + state: link + become: yes + +- name: Ensure NGINX is started + ansible.builtin.service: + name: nginx + state: restarted + become: yes +... diff --git a/ansible/roles/web/tasks/static.yml b/ansible/roles/web/tasks/static.yml new file mode 100644 index 0000000..24ac87a --- /dev/null +++ b/ansible/roles/web/tasks/static.yml @@ -0,0 +1,16 @@ +--- +- name: Ensure static_root directory exists + ansible.builtin.file: + path: "{{ python_environment.vars.static_root }}" + state: directory + owner: root + group: www-data + mode: '0775' + become: yes + +- name: Collect static files + ansible.builtin.command: + cmd: "{{ virtualenv_python_bin }} {{ project_source_dir }}/manage.py collectstatic --noinput" + environment: "{{ env_vars }}" + become: yes +... diff --git a/ansible/roles/web/templates/gunicorn_config.j2 b/ansible/roles/web/templates/gunicorn_config.j2 new file mode 100644 index 0000000..05290de --- /dev/null +++ b/ansible/roles/web/templates/gunicorn_config.j2 @@ -0,0 +1,14 @@ +import multiprocessing + +worker_class = 'gthread' +workers = multiprocessing.cpu_count() * 2 + 1 +threads = multiprocessing.cpu_count() * 2 + 1 + +bind = ['unix:{{ gunicorn_socket }}'] +timeout = "{{ gunicorn_timeout }}" +graceful_timeout = "{{ gunicorn_timeout }}" +keepalive = "{{ gunicorn_keepalive }}" + +chdir = "{{ project_source_dir }}" +error_log = "{{ gunicorn_error_log }}" +pidfile = "{{ gunicorn_pid_file }}" diff --git a/ansible/roles/web/templates/gunicorn_environment.j2 b/ansible/roles/web/templates/gunicorn_environment.j2 new file mode 100644 index 0000000..16f121d --- /dev/null +++ b/ansible/roles/web/templates/gunicorn_environment.j2 @@ -0,0 +1,4 @@ +{# Python environment variables #} +{% for key, value in env_vars.items() %} +{{ key }}="{{ value }}" +{% endfor %} diff --git a/ansible/roles/web/templates/nginx.j2 b/ansible/roles/web/templates/nginx.j2 new file mode 100644 index 0000000..2cba09e --- /dev/null +++ b/ansible/roles/web/templates/nginx.j2 @@ -0,0 +1,42 @@ +upstream {{ project_name }} { + server unix:{{ gunicorn_socket }} fail_timeout=0; +} + +server { + + listen 80 default_server; + listen [::]:80 default_server; + listen 443 ssl; + + server_name {{ web_domain }}; + + ssl_certificate {{ certbot_key_dir }}/fullchain.pem; + ssl_certificate_key {{ certbot_key_dir }}/privkey.pem; + include {{ certbot_conf_dir }}/options-ssl-nginx.conf; + ssl_dhparam {{ certbot_conf_dir }}/ssl-dhparams.pem; + + if ($scheme != "https") { + return 301 https://$host$request_uri; + } + + client_max_body_size 300M; + + root {{ project_www_dir }}; + + access_log {{ nginx_access_log }}; + error_log {{ nginx_error_log }}; + + location / { + try_files $uri @{{ project_name }}_proxy; + } + + location @{{ project_name }}_proxy { + proxy_pass http://{{ project_name }}; + proxy_redirect off; + proxy_read_timeout 45s; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} diff --git a/ansible/roles/web/templates/systemd_gunicorn.j2 b/ansible/roles/web/templates/systemd_gunicorn.j2 new file mode 100644 index 0000000..32a49cf --- /dev/null +++ b/ansible/roles/web/templates/systemd_gunicorn.j2 @@ -0,0 +1,23 @@ +[Unit] +Description={{ project_name }} gunicorn daemon +After=network.target +Requires={{ project_name }}_gunicorn.socket + +[Service] +Type=notify +DynamicUser=yes +Group=www-data +ConfigurationDirectory={{ project_name }}/gunicorn +RuntimeDirectory={{ project_name }}/gunicorn +LogsDirectory={{ project_name }}/gunicorn +WorkingDirectory={{ project_source_dir }} +EnvironmentFile={{ gunicorn_environment_path }} +ExecStart={{ virtualenv_bin_dir }}/gunicorn {{ project_name }}.wsgi -c {{ gunicorn_config_path }} +ExecReload=/bin/kill -s HUP $MAINPID +KillMode=mixed +TimeoutStopSec=5 +Restart=on-failure +RestartSec=3 + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/web/templates/systemd_gunicorn_socket.j2 b/ansible/roles/web/templates/systemd_gunicorn_socket.j2 new file mode 100644 index 0000000..2124421 --- /dev/null +++ b/ansible/roles/web/templates/systemd_gunicorn_socket.j2 @@ -0,0 +1,11 @@ +[Unit] +Description={{ project_name }} gunicorn socket + +[Socket] +ListenStream={{ gunicorn_socket }} +SocketUser=www-data +SocketGroup=www-data +SocketMode=660 + +[Install] +WantedBy=sockets.target diff --git a/ansible/roles/worker/defaults/main/celery.yml b/ansible/roles/worker/defaults/main/celery.yml new file mode 100644 index 0000000..21022a1 --- /dev/null +++ b/ansible/roles/worker/defaults/main/celery.yml @@ -0,0 +1,11 @@ +--- +celery_binary: "{{ virtualenv_bin_dir }}/celery" + +celery_service_name: "{{ project_name }}_celery" +celery_config_dir: "{{ project_config_dir }}/celery" +celery_config_path: "{{ celery_config_dir }}/config" +celery_runtime_dir: "{{ project_runtime_dir }}/celery" +celery_log_dir: "{{ project_log_dir }}/celery" +celery_log_level: "INFO" +celery_state_dir: "{{ project_state_dir }}/celery" +... diff --git a/ansible/roles/worker/defaults/main/celery_beat.yml b/ansible/roles/worker/defaults/main/celery_beat.yml new file mode 100644 index 0000000..c3c2f30 --- /dev/null +++ b/ansible/roles/worker/defaults/main/celery_beat.yml @@ -0,0 +1,8 @@ +--- +celery_beat_service_name: "{{ project_name }}_celery_beat" +celery_beat_config_dir: "{{ project_config_dir }}/celerybeat" +celery_beat_config_path: "{{ celery_beat_config_dir }}/config" +celery_beat_runtime_dir: "{{ project_runtime_dir }}/celerybeat" +celery_beat_log_dir: "{{ project_log_dir }}/celerybeat" +celery_beat_log_level: "INFO" +... diff --git a/ansible/roles/worker/handlers/main.yml b/ansible/roles/worker/handlers/main.yml new file mode 100644 index 0000000..140cb02 --- /dev/null +++ b/ansible/roles/worker/handlers/main.yml @@ -0,0 +1,15 @@ +--- +- name: Restart celery beat + ansible.builtin.service: + name: "{{ project_name }}_celery_beat" + state: restarted + listen: "restart celery beat" + become: yes + +- name: Restart celery workers + ansible.builtin.service: + name: "{{ project_name }}_celery" + state: restarted + listen: "restart celery workers" + become: yes +... diff --git a/ansible/roles/worker/meta/main.yml b/ansible/roles/worker/meta/main.yml new file mode 100644 index 0000000..5cc4aff --- /dev/null +++ b/ansible/roles/worker/meta/main.yml @@ -0,0 +1,9 @@ +--- +dependencies: + - role: common + - role: cache + - role: db + - role: queue + - role: python + - role: migrator +... diff --git a/ansible/roles/worker/tasks/celery.yml b/ansible/roles/worker/tasks/celery.yml new file mode 100644 index 0000000..44d58e0 --- /dev/null +++ b/ansible/roles/worker/tasks/celery.yml @@ -0,0 +1,36 @@ +--- +- name: Ensure celery config directory exists + ansible.builtin.file: + path: "{{ celery_config_dir }}" + state: directory + mode: '0755' + become: yes + +- name: Install celery env file + ansible.builtin.template: + src: templates/celery_env.j2 + dest: "{{ celery_config_path }}" + owner: root + group: www-data + mode: '0640' + become: yes + notify: "restart celery workers" + +- name: Install celery systemd service config + ansible.builtin.template: + src: templates/systemd_celery.j2 + dest: "{{ systemd_config_dir }}/{{ celery_service_name }}.service" + owner: root + group: root + mode: '0644' + become: yes + notify: "restart celery workers" + +- name: Enable and start celery + ansible.builtin.systemd: + name: "{{ celery_service_name }}.service" + enabled: yes + state: started + daemon_reload: yes + become: yes +... diff --git a/ansible/roles/worker/tasks/celery_beat.yml b/ansible/roles/worker/tasks/celery_beat.yml new file mode 100644 index 0000000..81d08ff --- /dev/null +++ b/ansible/roles/worker/tasks/celery_beat.yml @@ -0,0 +1,36 @@ +--- +- name: Ensure celery beat config directory exists + ansible.builtin.file: + path: "{{ celery_beat_config_dir }}" + state: directory + mode: '0755' + become: yes + +- name: Install celery beat env file + ansible.builtin.template: + src: templates/celery_beat_env.j2 + dest: "{{ celery_beat_config_path }}" + owner: root + group: www-data + mode: '0640' + become: yes + notify: "restart celery beat" + +- name: Install celery beat systemd service config + ansible.builtin.template: + src: templates/systemd_celery_beat.j2 + dest: "{{ systemd_config_dir }}/{{ celery_beat_service_name }}.service" + owner: root + group: root + mode: '0644' + become: yes + notify: "restart celery beat" + +- name: Enable and start celery beat + ansible.builtin.systemd: + name: "{{ celery_beat_service_name }}.service" + enabled: yes + state: started + daemon_reload: yes + become: yes +... diff --git a/ansible/roles/worker/tasks/main.yml b/ansible/roles/worker/tasks/main.yml new file mode 100644 index 0000000..a54030d --- /dev/null +++ b/ansible/roles/worker/tasks/main.yml @@ -0,0 +1,4 @@ +--- +- include_tasks: celery.yml +- include_tasks: celery_beat.yml +... diff --git a/ansible/roles/worker/templates/celery_beat_env.j2 b/ansible/roles/worker/templates/celery_beat_env.j2 new file mode 100644 index 0000000..e5646fd --- /dev/null +++ b/ansible/roles/worker/templates/celery_beat_env.j2 @@ -0,0 +1,12 @@ +{# Python environment variables #} +{% for key, value in env_vars.items() %} +{{ key }}="{{ value }}" +{% endfor %} + +{# Celery systemd environment variables #} +CELERY_BIN="{{ celery_binary }}" +CELERY_APP="{{ project_name }}" + +CELERYBEAT_PID_FILE="{{ celery_beat_runtime_dir }}/beat.pid" +CELERYBEAT_LOG_FILE="{{ celery_beat_log_dir }}/beat.log" +CELERYBEAT_LOG_LEVEL="{{ celery_beat_log_level }}" diff --git a/ansible/roles/worker/templates/celery_env.j2 b/ansible/roles/worker/templates/celery_env.j2 new file mode 100644 index 0000000..d0a7a62 --- /dev/null +++ b/ansible/roles/worker/templates/celery_env.j2 @@ -0,0 +1,13 @@ +{# Python environment variables #} +{% for key, value in env_vars.items() %} +{{ key }}="{{ value }}" +{% endfor %} + +{# Celery systemd environment variables #} +CELERY_BIN="{{ celery_binary }}" +CELERY_APP="{{ project_name }}" +CELERYD_NODES="worker1" +CELERYD_PID_FILE="{{ celery_runtime_dir }}/%n.pid" +CELERYD_LOG_FILE="{{ celery_log_dir }}/%n%I.log" +CELERYD_LOG_LEVEL="{{ celery_log_level }}" +CELERYD_STATE_DB="{{ celery_state_dir }}/db.state" diff --git a/ansible/roles/worker/templates/systemd_celery.j2 b/ansible/roles/worker/templates/systemd_celery.j2 new file mode 100644 index 0000000..f885bf7 --- /dev/null +++ b/ansible/roles/worker/templates/systemd_celery.j2 @@ -0,0 +1,33 @@ +[Unit] +Description={{ project_name }} celery service +After=network.target rabbitmq-server.service +Requires=rabbitmq-server.service + +[Service] +Type=forking +DynamicUser=yes +Group=www-data +ConfigurationDirectory={{ project_name }}/celery +RuntimeDirectory={{ project_name }}/celery +LogsDirectory={{ project_name }}/celery +StateDirectory={{ project_name }}/celery +WorkingDirectory={{ project_source_dir }} +EnvironmentFile={{ celery_config_path }} +ExecStart=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi start $CELERYD_NODES \ + --pidfile=${CELERYD_PID_FILE} \ + --logfile=${CELERYD_LOG_FILE} \ + --loglevel=${CELERYD_LOG_LEVEL} \ + --statedb=${CELERYD_STATE_DB} \ + $CELERYD_OPTS' +ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait $CELERYD_NODES \ + --pidfile=${CELERYD_PID_FILE} \ + --loglevel=${CELERYD_LOG_LEVEL}' +ExecReload=/bin/sh -c '${CELERY_BIN} -A $CELERY_APP multi restart $CELERYD_NODES \ + --pidfile=${CELERYD_PID_FILE} \ + --logfile=${CELERYD_LOG_FILE} \ + --loglevel=${CELERYD_LOG_LEVEL} \ + $CELERYD_OPTS' +Restart=on-failure + +[Install] +WantedBy=multi-user.target diff --git a/ansible/roles/worker/templates/systemd_celery_beat.j2 b/ansible/roles/worker/templates/systemd_celery_beat.j2 new file mode 100644 index 0000000..4b09027 --- /dev/null +++ b/ansible/roles/worker/templates/systemd_celery_beat.j2 @@ -0,0 +1,21 @@ +[Unit] +Description={{ project_name }} celery beat service +After=network.target + +[Service] +Type=simple +DynamicUser=yes +Group=www-data +LogsDirectory={{ project_name }}/celerybeat +ConfigurationDirectory={{ project_name }}/celerybeat +RuntimeDirectory={{ project_name }}/celerybeat +WorkingDirectory={{ project_source_dir }} +EnvironmentFile={{ celery_config_path }} +ExecStart=/bin/sh -c '${CELERY_BIN} -A ${CELERY_APP} beat \ + --pidfile=${CELERYBEAT_PID_FILE} \ + --logfile=${CELERYBEAT_LOG_FILE} \ + --loglevel=${CELERYBEAT_LOG_LEVEL}' +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/ansible/ssh_config.j2 b/ansible/ssh_config.j2 new file mode 100644 index 0000000..abb67aa --- /dev/null +++ b/ansible/ssh_config.j2 @@ -0,0 +1,20 @@ +Host {{ ssh_host }} + User {{ ssh_user }} + HostName {{ ssh_host }} + Port {{ ssh_port | default(22, true) }} + ControlMaster auto + ControlPersist 10m + ControlPath /tmp/cm-%r:%h:%p + {% if ssh_jump_host %}ProxyJump {{ ssh_jump_host }}{% endif %} + +{% if ssh_jump_host %} + +Host {{ ssh_jump_host }} + User {{ ssh_jump_user | default(ssh_user, true) }} + HostName {{ ssh_jump_host }} + Port {{ ssh_jump_port | default(22, true) }} + ForwardAgent yes + ControlMaster auto + ControlPersist 10m + ControlPath /tmp/cm-%r:%h:%p +{% endif %} diff --git a/ansible/ssh_config.yml b/ansible/ssh_config.yml new file mode 100644 index 0000000..d44a5f1 --- /dev/null +++ b/ansible/ssh_config.yml @@ -0,0 +1,38 @@ +--- +- name: SSH Configuration Playbook + hosts: localhost + connection: local + gather_facts: false + vars: + ssh_host: "{{ lookup('env', 'SSH_HOST') }}" + ssh_user: "{{ lookup('env', 'SSH_USER') }}" + ssh_port: "{{ lookup('env', 'SSH_PORT') | default(22, true) }}" + ssh_jump_host: "{{ lookup('env', 'SSH_JUMP_HOST') }}" + ssh_jump_user: "{{ lookup('env', 'SSH_JUMP_USER') | default(ssh_user, true) }}" + ssh_jump_port: "{{ lookup('env', 'SSH_JUMP_PORT') | default(22, true) }}" + ssh_private_key: "{{ lookup('env', 'SSH_PRIVATE_KEY') }}" + ssh_known_hosts: "{{ lookup('env', 'SSH_KNOWN_HOSTS') }}" + ssh_key_type: "{{ lookup('env', 'SSH_PRIVATE_KEY_TYPE') | default('rsa', true) }}" + home_dir: "{{ lookup('env', 'HOME') }}" + tasks: + - name: Ensure SSH directory exists + ansible.builtin.file: + path: "{{ home_dir }}/.ssh" + state: directory + mode: '0755' + - name: Install SSH config file + ansible.builtin.template: + src: ssh_config.j2 + dest: "{{ home_dir }}/.ssh/config" + mode: '0600' + - name: Install SSH private key file + ansible.builtin.copy: + content: "{{ ssh_private_key }}\n" + dest: "{{ home_dir }}/.ssh/id_{{ ssh_key_type }}" + mode: '0600' + - name: Install SSH known_hosts file + ansible.builtin.copy: + content: "{{ ssh_known_hosts }}" + dest: "{{ home_dir }}/.ssh/known_hosts" + mode: '0600' +... diff --git a/github/workflows/deploy.yml b/github/workflows/deploy.yml new file mode 100644 index 0000000..dff9f99 --- /dev/null +++ b/github/workflows/deploy.yml @@ -0,0 +1,46 @@ +--- +name: Deploy Project + +on: + release: + types: [released] + +jobs: + deploy: + name: Deploy project + runs-on: ubuntu-20.04 + + steps: + - name: Checkout project + uses: actions/checkout@v2 + + - name: Create SSH configuration + uses: Apreche/action-ansible-playbook@v3.0.0 + with: + playbook: ssh_config.yml + inventory: | + localhost + env: + SSH_HOST: ${{secrets.ANSIBLE_SSH_HOST}} + SSH_USER: ${{secrets.ANSIBLE_SSH_USER}} + SSH_PORT: ${{secrets.ANSIBLE_SSH_PORT}} + SSH_JUMP_HOST: ${{secrets.ANSIBLE_SSH_JUMP_HOST}} + SSH_JUMP_USER: ${{secrets.ANSIBLE_SSH_JUMP_USER}} + SSH_JUMP_PORT: ${{secrets.ANSIBLE_SSH_JUMP_PORT}} + SSH_PRIVATE_KEY: ${{secrets.ANSIBLE_SSH_PRIVATE_KEY}} + SSH_PRIVATE_KEY_TYPE: ${{secrets.ANSIBLE_SSH_PRIVATE_KEY_TYPE}} + SSH_KNOWN_HOSTS: ${{secrets.ANSIBLE_SSH_KNOWN_HOSTS}} + + - name: Execute deployment playbook + uses: Apreche/action-ansible-playbook@v3.0.0 + with: + playbook: deploy.yml + vault_password: ${{secrets.ANSIBLE_VAULT_PASSWORD}} + inventory: | + [all] + ${{secrets.ANSIBLE_SSH_HOST}} + env: + DEPLOY_SSH_PRIVATE_KEY: ${{secrets.DEPLOY_SSH_PRIVATE_KEY}} + WEB_DOMAIN: ${{secrets.WEB_DOMAIN}} + EMAIL_ADDRESS: ${{secrets.EMAIL_ADDRESS}} +... diff --git a/github/workflows/lint.yml b/github/workflows/lint.yml new file mode 100644 index 0000000..0da6355 --- /dev/null +++ b/github/workflows/lint.yml @@ -0,0 +1,28 @@ +--- +name: Lint Code Base + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + name: Lint Code Base + runs-on: ubuntu-20.04 + + steps: + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Lint Code Base + uses: github/super-linter@v3 + env: + VALIDATE_ALL_CODEBASE: false + IGNORE_GITIGNORED_FILES: true + DEFAULT_BRANCH: main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +... diff --git a/github/workflows/test.yml b/github/workflows/test.yml new file mode 100644 index 0000000..021f636 --- /dev/null +++ b/github/workflows/test.yml @@ -0,0 +1,88 @@ +name: Django Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-20.04 + + services: + postgres: + image: postgres:latest + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + memcached: + image: memcached:latest + ports: + - 11211:11211 + options: >- + --health-cmd "timeout 5 bash -c 'cat < /dev/null > /dev/udp/127.0.0.1/11211'" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + rabbitmq: + image: rabbitmq:latest + env: + RABBITMQ_DEFAULT_USER: test + RABBITMQ_DEFAULT_PASSWORD: test + RABBITMQ_DEFAULT_VHOST: test + ports: + - 5672:5672 + options: >- + --health-cmd "rabbitmqctl node_health_check" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v2 + + - name: Install Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Install prerequisites + run: | + sudo apt-get install libmemcached-dev + + - name: Load Python packages from cache + uses: actions/cache@v2 + id: pipcache + with: + {% verbatim %}path: ${% templatetag openvariable %} env.pythonLocation }} + key: ${{ runner.os }}-pip-${{ env.pythonLocation }}-${{ hashFiles('poetry.lock') }}{% endverbatim %} + + - name: Install Python packages + run: | + python -m pip install --upgrade pip + pip install poetry + poetry export > ~/requirements.txt + pip install -r ~/requirements.txt + + - name: Run Django tests + env: + {{ project_name | upper }}_DEBUG: False + {{ project_name | upper }}_DB_NAME: test + {{ project_name | upper }}_DB_USER: test + {{ project_name | upper }}_DB_PASSWORD: test + {{ project_name | upper }}_DB_HOST: 127.0.0.1 + {{ project_name | upper }}_DB_PORT: 5432 + {{ project_name | upper }}_MEMCACHED_SOCKET: 127.0.0.1:11211 + {{ project_name | upper }}_CELERY_USER: test + {{ project_name | upper }}_CELERY_PASSWORD: test + {{ project_name | upper }}_CELERY_HOST: 127.0.0.1 + {{ project_name | upper }}_CELERY_VHOST: test + run: | + python manage.py test diff --git a/gitignore b/gitignore new file mode 100644 index 0000000..08a1c9b --- /dev/null +++ b/gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# We use poetry, so ignore requirements.txt +requirements.txt diff --git a/manage.py-tpl b/manage.py-tpl new file mode 100755 index 0000000..77746e5 --- /dev/null +++ b/manage.py-tpl @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..603af52 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,775 @@ +[[package]] +name = "amqp" +version = "5.0.6" +description = "Low-level AMQP client for Python (fork of amqplib)." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +vine = "5.0.0" + +[[package]] +name = "appnope" +version = "0.1.2" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "billiard" +version = "3.6.4.0" +description = "Python multiprocessing fork with improvements and bugfixes" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "celery" +version = "5.1.2" +description = "Distributed Task Queue." +category = "main" +optional = false +python-versions = ">=3.6," + +[package.dependencies] +billiard = ">=3.6.4.0,<4.0" +click = ">=7.0,<8.0" +click-didyoumean = ">=0.0.3" +click-plugins = ">=1.1.1" +click-repl = ">=0.1.6" +kombu = ">=5.1.0,<6.0" +pytz = ">0.0-dev" +vine = ">=5.0.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=1.3.2)"] +auth = ["cryptography"] +azureblockblob = ["azure-storage-blob (==12.6.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (<3.21.0)"] +consul = ["python-consul2"] +cosmosdbsql = ["pydocumentdb (==2.3.2)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb"] +django = ["Django (>=1.11)"] +dynamodb = ["boto3 (>=1.9.178)"] +elasticsearch = ["elasticsearch"] +eventlet = ["eventlet (>=0.26.1)"] +gevent = ["gevent (>=1.0.0)"] +librabbitmq = ["librabbitmq (>=1.5.0)"] +memcache = ["pylibmc"] +mongodb = ["pymongo[srv] (>=3.3.0)"] +msgpack = ["msgpack"] +pymemcache = ["python-memcached"] +pyro = ["pyro4"] +pytest = ["pytest-celery"] +redis = ["redis (>=3.2.0)"] +s3 = ["boto3 (>=1.9.125)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.9.125)", "pycurl (==7.43.0.5)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard"] + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "click-didyoumean" +version = "0.0.3" +description = "Enable git-like did-you-mean feature in click." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["pytest (>=3.6)", "pytest-cov", "wheel", "coveralls"] + +[[package]] +name = "click-repl" +version = "0.2.0" +description = "REPL plugin for Click" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = "*" +prompt-toolkit = "*" +six = "*" + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "decorator" +version = "5.0.9" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "django" +version = "3.2.5" +description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +asgiref = ">=3.3.2,<4" +pytz = "*" +sqlparse = ">=0.2.2" + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-celery-beat" +version = "2.2.1" +description = "Database-backed Periodic Tasks." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +celery = ">=5.0,<6.0" +Django = ">=2.2,<4.0" +django-timezone-field = ">=4.1.0,<5.0" +python-crontab = ">=2.3.4" + +[[package]] +name = "django-celery-results" +version = "2.2.0" +description = "Celery result backends for Django." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +celery = ">=5.0,<6.0" + +[[package]] +name = "django-debug-toolbar" +version = "3.2.1" +description = "A configurable set of panels that display various debug information about the current request/response." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" +sqlparse = ">=0.2.0" + +[[package]] +name = "django-extensions" +version = "3.1.3" +description = "Extensions for Django" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Django = ">=2.2" + +[[package]] +name = "django-timezone-field" +version = "4.2.1" +description = "A Django app providing database and form fields for pytz timezone objects." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +django = ">=2.2" +pytz = "*" + +[package.extras] +rest_framework = ["djangorestframework (>=3.0.0)"] + +[[package]] +name = "docutils" +version = "0.16" +description = "Docutils -- Python Documentation Utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "ipdb" +version = "0.13.9" +description = "IPython-enabled pdb" +category = "dev" +optional = false +python-versions = ">=2.7" + +[package.dependencies] +decorator = {version = "*", markers = "python_version > \"3.6\""} +ipython = {version = ">=7.17.0", markers = "python_version > \"3.6\""} +toml = {version = ">=0.10.2", markers = "python_version > \"3.6\""} + +[[package]] +name = "ipython" +version = "7.25.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=2.0.0,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.1.0" +pygments = "*" +traitlets = ">=4.2" + +[package.extras] +all = ["Sphinx (>=1.3)", "ipykernel", "ipyparallel", "ipywidgets", "nbconvert", "nbformat", "nose (>=0.10.1)", "notebook", "numpy (>=1.17)", "pygments", "qtconsole", "requests", "testpath"] +doc = ["Sphinx (>=1.3)"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["notebook", "ipywidgets"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["nose (>=0.10.1)", "requests", "testpath", "pygments", "nbformat", "ipykernel", "numpy (>=1.17)"] + +[[package]] +name = "ipython-genutils" +version = "0.2.0" +description = "Vestigial utilities from IPython" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "jedi" +version = "0.18.0" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +parso = ">=0.8.0,<0.9.0" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<6.0.0)"] + +[[package]] +name = "kombu" +version = "5.1.0" +description = "Messaging library for Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +amqp = ">=5.0.6,<6.0.0" +vine = "*" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.0.0)"] +azurestoragequeues = ["azure-storage-queue"] +consul = ["python-consul (>=0.6.0)"] +librabbitmq = ["librabbitmq (>=1.5.2)"] +mongodb = ["pymongo (>=3.3.0)"] +msgpack = ["msgpack"] +pyro = ["pyro4"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=3.3.11)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy"] +sqs = ["boto3 (>=1.4.4)", "pycurl (==7.43.0.2)", "urllib3 (<1.26)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.2" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +traitlets = "*" + +[[package]] +name = "parso" +version = "0.8.2" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pillow" +version = "8.3.1" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "prompt-toolkit" +version = "3.0.19" +description = "Library for building powerful interactive command lines in Python" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psycopg2" +version = "2.9.1" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "pygments" +version = "2.9.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "pylibmc" +version = "1.6.1" +description = "Quick and small memcached client for Python" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "python-crontab" +version = "2.5.1" +description = "Python Crontab API" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +python-dateutil = "*" + +[package.extras] +cron-description = ["cron-descriptor"] +cron-schedule = ["croniter"] + +[[package]] +name = "python-dateutil" +version = "2.8.1" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2021.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "sqlparse" +version = "0.4.1" +description = "A non-validating SQL parser." +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "traitlets" +version = "5.0.5" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +ipython-genutils = "*" + +[package.extras] +test = ["pytest"] + +[[package]] +name = "vine" +version = "5.0.0" +description = "Promises, promises, promises." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "werkzeug" +version = "1.0.1" +description = "The comprehensive WSGI web application library." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["pytest", "pytest-timeout", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] +watchdog = ["watchdog"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "b6446944f0f4b308a9ef8a419a24d859933dfb14449311ecf1e7ca947d2adac5" + +[metadata.files] +amqp = [ + {file = "amqp-5.0.6-py3-none-any.whl", hash = "sha256:493a2ac6788ce270a2f6a765b017299f60c1998f5a8617908ee9be082f7300fb"}, + {file = "amqp-5.0.6.tar.gz", hash = "sha256:03e16e94f2b34c31f8bf1206d8ddd3ccaa4c315f7f6a1879b7b1210d229568c2"}, +] +appnope = [ + {file = "appnope-0.1.2-py2.py3-none-any.whl", hash = "sha256:93aa393e9d6c54c5cd570ccadd8edad61ea0c4b9ea7a01409020c9aa019eb442"}, + {file = "appnope-0.1.2.tar.gz", hash = "sha256:dd83cd4b5b460958838f6eb3000c660b1f9caf2a5b1de4264e941512f603258a"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +backcall = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] +billiard = [ + {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, + {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, +] +celery = [ + {file = "celery-5.1.2-py3-none-any.whl", hash = "sha256:9dab2170b4038f7bf10ef2861dbf486ddf1d20592290a1040f7b7a1259705d42"}, + {file = "celery-5.1.2.tar.gz", hash = "sha256:8d9a3de9162965e97f8e8cc584c67aad83b3f7a267584fa47701ed11c3e0d4b0"}, +] +click = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] +click-didyoumean = [ + {file = "click-didyoumean-0.0.3.tar.gz", hash = "sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"}, +] +click-plugins = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] +click-repl = [ + {file = "click-repl-0.2.0.tar.gz", hash = "sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"}, + {file = "click_repl-0.2.0-py3-none-any.whl", hash = "sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +decorator = [ + {file = "decorator-5.0.9-py3-none-any.whl", hash = "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323"}, + {file = "decorator-5.0.9.tar.gz", hash = "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5"}, +] +django = [ + {file = "Django-3.2.5-py3-none-any.whl", hash = "sha256:c58b5f19c5ae0afe6d75cbdd7df561e6eb929339985dbbda2565e1cabb19a62e"}, + {file = "Django-3.2.5.tar.gz", hash = "sha256:3da05fea54fdec2315b54a563d5b59f3b4e2b1e69c3a5841dda35019c01855cd"}, +] +django-celery-beat = [ + {file = "django-celery-beat-2.2.1.tar.gz", hash = "sha256:97ae5eb309541551bdb07bf60cc57cadacf42a74287560ced2d2c06298620234"}, + {file = "django_celery_beat-2.2.1-py2.py3-none-any.whl", hash = "sha256:ab43049634fd18dc037927d7c2c7d5f67f95283a20ebbda55f42f8606412e66c"}, +] +django-celery-results = [ + {file = "django_celery_results-2.2.0-py2.py3-none-any.whl", hash = "sha256:d5f83fad9091e52cd6dbb3ca80632153ad14b6cdac4d73258e040f92717237cb"}, + {file = "django_celery_results-2.2.0.tar.gz", hash = "sha256:cc0285090a306f97f1d4b7929ed98af0475bf6db2568976b3387de4fbe812edc"}, +] +django-debug-toolbar = [ + {file = "django-debug-toolbar-3.2.1.tar.gz", hash = "sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33"}, + {file = "django_debug_toolbar-3.2.1-py3-none-any.whl", hash = "sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"}, +] +django-extensions = [ + {file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"}, + {file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"}, +] +django-timezone-field = [ + {file = "django-timezone-field-4.2.1.tar.gz", hash = "sha256:97780cde658daa5094ae515bb55ca97c1352928ab554041207ad515dee3fe971"}, + {file = "django_timezone_field-4.2.1-py3-none-any.whl", hash = "sha256:6dc782e31036a58da35b553bd00c70f112d794700025270d8a6a4c1d2e5b26c6"}, +] +docutils = [ + {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, + {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, +] +gunicorn = [ + {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, + {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, +] +ipdb = [ + {file = "ipdb-0.13.9.tar.gz", hash = "sha256:951bd9a64731c444fd907a5ce268543020086a697f6be08f7cc2c9a752a278c5"}, +] +ipython = [ + {file = "ipython-7.25.0-py3-none-any.whl", hash = "sha256:aa21412f2b04ad1a652e30564fff6b4de04726ce875eab222c8430edc6db383a"}, + {file = "ipython-7.25.0.tar.gz", hash = "sha256:54bbd1fe3882457aaf28ae060a5ccdef97f212a741754e420028d4ec5c2291dc"}, +] +ipython-genutils = [ + {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, + {file = "ipython_genutils-0.2.0.tar.gz", hash = "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"}, +] +jedi = [ + {file = "jedi-0.18.0-py2.py3-none-any.whl", hash = "sha256:18456d83f65f400ab0c2d3319e48520420ef43b23a086fdc05dff34132f0fb93"}, + {file = "jedi-0.18.0.tar.gz", hash = "sha256:92550a404bad8afed881a137ec9a461fed49eca661414be45059329614ed0707"}, +] +kombu = [ + {file = "kombu-5.1.0-py3-none-any.whl", hash = "sha256:e2dedd8a86c9077c350555153825a31e456a0dc20c15d5751f00137ec9c75f0a"}, + {file = "kombu-5.1.0.tar.gz", hash = "sha256:01481d99f4606f6939cdc9b637264ed353ee9e3e4f62cfb582324142c41a572d"}, +] +matplotlib-inline = [ + {file = "matplotlib-inline-0.1.2.tar.gz", hash = "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e"}, + {file = "matplotlib_inline-0.1.2-py3-none-any.whl", hash = "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811"}, +] +parso = [ + {file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"}, + {file = "parso-0.8.2.tar.gz", hash = "sha256:12b83492c6239ce32ff5eed6d3639d6a536170723c6f3f1506869f1ace413398"}, +] +pexpect = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] +pickleshare = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] +pillow = [ + {file = "Pillow-8.3.1-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:196560dba4da7a72c5e7085fccc5938ab4075fd37fe8b5468869724109812edd"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c9569049d04aaacd690573a0398dbd8e0bf0255684fee512b413c2142ab723"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c088a000dfdd88c184cc7271bfac8c5b82d9efa8637cd2b68183771e3cf56f04"}, + {file = "Pillow-8.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fc214a6b75d2e0ea7745488da7da3c381f41790812988c7a92345978414fad37"}, + {file = "Pillow-8.3.1-cp36-cp36m-win32.whl", hash = "sha256:a17ca41f45cf78c2216ebfab03add7cc350c305c38ff34ef4eef66b7d76c5229"}, + {file = "Pillow-8.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:67b3666b544b953a2777cb3f5a922e991be73ab32635666ee72e05876b8a92de"}, + {file = "Pillow-8.3.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:ff04c373477723430dce2e9d024c708a047d44cf17166bf16e604b379bf0ca14"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9364c81b252d8348e9cc0cb63e856b8f7c1b340caba6ee7a7a65c968312f7dab"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2f381932dca2cf775811a008aa3027671ace723b7a38838045b1aee8669fdcf"}, + {file = "Pillow-8.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d0da39795049a9afcaadec532e7b669b5ebbb2a9134576ebcc15dd5bdae33cc0"}, + {file = "Pillow-8.3.1-cp37-cp37m-win32.whl", hash = "sha256:2b6dfa068a8b6137da34a4936f5a816aba0ecc967af2feeb32c4393ddd671cba"}, + {file = "Pillow-8.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4eef1ff2d62676deabf076f963eda4da34b51bc0517c70239fafed1d5b51500"}, + {file = "Pillow-8.3.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:660a87085925c61a0dcc80efb967512ac34dbb256ff7dd2b9b4ee8dbdab58cf4"}, + {file = "Pillow-8.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:15a2808e269a1cf2131930183dcc0419bc77bb73eb54285dde2706ac9939fa8e"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:969cc558cca859cadf24f890fc009e1bce7d7d0386ba7c0478641a60199adf79"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2ee77c14a0299d0541d26f3d8500bb57e081233e3fa915fa35abd02c51fa7fae"}, + {file = "Pillow-8.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c11003197f908878164f0e6da15fce22373ac3fc320cda8c9d16e6bba105b844"}, + {file = "Pillow-8.3.1-cp38-cp38-win32.whl", hash = "sha256:3f08bd8d785204149b5b33e3b5f0ebbfe2190ea58d1a051c578e29e39bfd2367"}, + {file = "Pillow-8.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:70af7d222df0ff81a2da601fab42decb009dc721545ed78549cb96e3a1c5f0c8"}, + {file = "Pillow-8.3.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:37730f6e68bdc6a3f02d2079c34c532330d206429f3cee651aab6b66839a9f0e"}, + {file = "Pillow-8.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bc3c7ef940eeb200ca65bd83005eb3aae8083d47e8fcbf5f0943baa50726856"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c35d09db702f4185ba22bb33ef1751ad49c266534339a5cebeb5159d364f6f82"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b2efa07f69dc395d95bb9ef3299f4ca29bcb2157dc615bae0b42c3c20668ffc"}, + {file = "Pillow-8.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cc866706d56bd3a7dbf8bac8660c6f6462f2f2b8a49add2ba617bc0c54473d83"}, + {file = "Pillow-8.3.1-cp39-cp39-win32.whl", hash = "sha256:9a211b663cf2314edbdb4cf897beeb5c9ee3810d1d53f0e423f06d6ebbf9cd5d"}, + {file = "Pillow-8.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:c2a5ff58751670292b406b9f06e07ed1446a4b13ffced6b6cab75b857485cbc8"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c379425c2707078dfb6bfad2430728831d399dc95a7deeb92015eb4c92345eaf"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:114f816e4f73f9ec06997b2fde81a92cbf0777c9e8f462005550eed6bae57e63"}, + {file = "Pillow-8.3.1-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8960a8a9f4598974e4c2aeb1bff9bdd5db03ee65fd1fce8adf3223721aa2a636"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:147bd9e71fb9dcf08357b4d530b5167941e222a6fd21f869c7911bac40b9994d"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1fd5066cd343b5db88c048d971994e56b296868766e461b82fa4e22498f34d77"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4ebde71785f8bceb39dcd1e7f06bcc5d5c3cf48b9f69ab52636309387b097c8"}, + {file = "Pillow-8.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1c03e24be975e2afe70dfc5da6f187eea0b49a68bb2b69db0f30a61b7031cee4"}, + {file = "Pillow-8.3.1.tar.gz", hash = "sha256:2cac53839bfc5cece8fdbe7f084d5e3ee61e1303cccc86511d351adcb9e2c792"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.19-py3-none-any.whl", hash = "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88"}, + {file = "prompt_toolkit-3.0.19.tar.gz", hash = "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f"}, +] +psycopg2 = [ + {file = "psycopg2-2.9.1-cp36-cp36m-win32.whl", hash = "sha256:7f91312f065df517187134cce8e395ab37f5b601a42446bdc0f0d51773621854"}, + {file = "psycopg2-2.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:830c8e8dddab6b6716a4bf73a09910c7954a92f40cf1d1e702fb93c8a919cc56"}, + {file = "psycopg2-2.9.1-cp37-cp37m-win32.whl", hash = "sha256:89409d369f4882c47f7ea20c42c5046879ce22c1e4ea20ef3b00a4dfc0a7f188"}, + {file = "psycopg2-2.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7640e1e4d72444ef012e275e7b53204d7fab341fb22bc76057ede22fe6860b25"}, + {file = "psycopg2-2.9.1-cp38-cp38-win32.whl", hash = "sha256:079d97fc22de90da1d370c90583659a9f9a6ee4007355f5825e5f1c70dffc1fa"}, + {file = "psycopg2-2.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:2c992196719fadda59f72d44603ee1a2fdcc67de097eea38d41c7ad9ad246e62"}, + {file = "psycopg2-2.9.1-cp39-cp39-win32.whl", hash = "sha256:2087013c159a73e09713294a44d0c8008204d06326006b7f652bef5ace66eebb"}, + {file = "psycopg2-2.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf35a25f1aaa8a3781195595577fcbb59934856ee46b4f252f56ad12b8043bcf"}, + {file = "psycopg2-2.9.1.tar.gz", hash = "sha256:de5303a6f1d0a7a34b9d40e4d3bef684ccc44a49bbe3eb85e3c0bffb4a131b7c"}, +] +ptyprocess = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] +pygments = [ + {file = "Pygments-2.9.0-py3-none-any.whl", hash = "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"}, + {file = "Pygments-2.9.0.tar.gz", hash = "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f"}, +] +pylibmc = [ + {file = "pylibmc-1.6.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:01a7e2e3fa9fcd7a791c7818a80a07e7a381aee988a5d810a1c1e6f7a9a288fd"}, + {file = "pylibmc-1.6.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6fff384e3c30af029bbac87f88b3fab14ae87b50103d389341d9b3e633349a3f"}, + {file = "pylibmc-1.6.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c749b4251c1137837d00542b62992b96cd2aed639877407f66291120dd6de2ff"}, + {file = "pylibmc-1.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e6c0c452336db0868d0de521d48872c2a359b1233b974c6b32c36ce68abc4820"}, + {file = "pylibmc-1.6.1.tar.gz", hash = "sha256:8a8dd406487d419d58c6d944efd91e8189b360a0c4d9e8c6ebe3990d646ae7e9"}, +] +python-crontab = [ + {file = "python-crontab-2.5.1.tar.gz", hash = "sha256:4bbe7e720753a132ca4ca9d4094915f40e9d9dc8a807a4564007651018ce8c31"}, +] +python-dateutil = [ + {file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"}, + {file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"}, +] +pytz = [ + {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, + {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +sqlparse = [ + {file = "sqlparse-0.4.1-py3-none-any.whl", hash = "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0"}, + {file = "sqlparse-0.4.1.tar.gz", hash = "sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +traitlets = [ + {file = "traitlets-5.0.5-py3-none-any.whl", hash = "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426"}, + {file = "traitlets-5.0.5.tar.gz", hash = "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396"}, +] +vine = [ + {file = "vine-5.0.0-py2.py3-none-any.whl", hash = "sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30"}, + {file = "vine-5.0.0.tar.gz", hash = "sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +werkzeug = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] diff --git a/project_name/__init__.py-tpl b/project_name/__init__.py-tpl new file mode 100644 index 0000000..fb989c4 --- /dev/null +++ b/project_name/__init__.py-tpl @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/project_name/asgi.py-tpl b/project_name/asgi.py-tpl new file mode 100644 index 0000000..27d3fbe --- /dev/null +++ b/project_name/asgi.py-tpl @@ -0,0 +1,16 @@ +""" +ASGI config for {{ project_name }} project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") + +application = get_asgi_application() diff --git a/project_name/celery.py-tpl b/project_name/celery.py-tpl new file mode 100644 index 0000000..b2cb828 --- /dev/null +++ b/project_name/celery.py-tpl @@ -0,0 +1,15 @@ +import os + +from celery import Celery + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") + +app = Celery("{{ project_name }}") + +app.config_from_object("django.conf:settings", namespace="CELERY") + +app.autodiscover_tasks() + +@app.task(bind=True) +def debug_task(self): + print(f"Request: {self.request!r}") diff --git a/project_name/settings.py-tpl b/project_name/settings.py-tpl new file mode 100644 index 0000000..5a540b7 --- /dev/null +++ b/project_name/settings.py-tpl @@ -0,0 +1,207 @@ +""" +Django settings for {{ project_name }} project. + +Generated by 'django-admin startproject' using Django {{ django_version }}. + +For more information on this file, see +https://docs.djangoproject.com/en/{{ docs_version }}/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/ +""" + +import os +from pathlib import Path + +from . import utils + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = os.environ.get( + "{{ project_name | upper }}_SECRET_KEY", + "{{ secret_key }}", +) + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = utils.str_to_bool(os.environ.get("{{ project_name | upper }}_DEBUG", "True")) + + +ALLOWED_HOSTS = [] +extra_hosts = os.environ.get("{{ project_name | upper }}_HOSTS", None) +if extra_hosts: + ALLOWED_HOSTS += extra_hosts.split(",") +INTERNAL_IPS = ["127.0.0.1"] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.admindocs", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_celery_beat", + "django_celery_results", + "django_extensions", +] + + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.contrib.admindocs.middleware.XViewMiddleware", +] + +if DEBUG: + INSTALLED_APPS += [ + "debug_toolbar", + ] + MIDDLEWARE = [ + "debug_toolbar.middleware.DebugToolbarMiddleware", + ] + MIDDLEWARE + +ROOT_URLCONF = "{{ project_name }}.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "{{ project_name }}.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#databases + +if DEBUG: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "db.sqlite3", + } + } +else: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.environ.get("{{ project_name | upper }}_DB_NAME", "{{ project_name }}"), + "USER": os.environ.get("{{ project_name | upper }}_DB_USER", "{{ project_name }}"), + "PASSWORD": os.environ.get("{{ project_name | upper }}_DB_PASSWORD", "{{ project_name }}"), + "HOST": os.environ.get("{{ project_name | upper }}_DB_HOST", "localhost"), + "PORT": os.environ.get("{{ project_name | upper }}_DB_PORT", "5432"), + } + } + +# Cache +if DEBUG: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.dummy.DummyCache", + } + } +else: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.PyLibMCCache", + "LOCATION": os.environ.get( + "{{ project_name | upper }}_MEMCACHED_SOCKET", + "/tmp/memcached.sock" + ) + } + } + + +# Password validation +# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/{{ docs_version }}/howto/static-files/ +STATICFILES_STORAGE = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage" +STATIC_URL = os.environ.get("{{ project_name | upper }}_STATIC_URL", "/static/") +STATIC_ROOT = os.environ.get("{{ project_name | upper }}_STATIC_ROOT", "/tmp/static/") +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] + +# Media files (User Uploads) +# https://docs.djangoproject.com/en/{{ docs_version }}/topics/files/ + +MEDIA_URL = os.environ.get("{{ project_name | upper }}_MEDIA_URL", "/media/") +MEDIA_ROOT = os.environ.get("{{ project_name | upper }}_MEDIA_ROOT", "/tmp/media/") + +# Celery +CELERY_TASK_ALWAYS_EAGER = DEBUG +CELERY_TASK_EAGER_PROPAGATES = DEBUG +CELERY_TASK_REMOTE_TRACEBACKS = DEBUG +CELERY_RESULT_BACKEND = "django-db" + +if not DEBUG: + celery_user = os.environ.get("{{ project_name | upper }}_CELERY_USER", None) + celery_password = os.environ.get("{{ project_name | upper }}_CELERY_PASSWORD", None) + celery_host = os.environ.get("{{ project_name | upper }}_CELERY_HOST", "127.0.0.1") + celery_port = os.environ.get("{{ project_name | upper }}_CELERY_PORT", 5672) + celery_vhost = os.environ.get("{{ project_name | upper }}_CELERY_VHOST", None) + if not any([var is None for var in (celery_user, celery_password, celery_vhost)]): + CELERY_BROKER_URL = ( + f"amqp://{celery_user}" + f":{celery_password}" + f"@{celery_host}" + f":{celery_port}" + f"/{celery_vhost}" + ) + + CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" diff --git a/project_name/urls.py-tpl b/project_name/urls.py-tpl new file mode 100644 index 0000000..7ed70a0 --- /dev/null +++ b/project_name/urls.py-tpl @@ -0,0 +1,40 @@ +"""{{ project_name }} URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/{{ docs_version }}/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path +from django.views.generic.base import TemplateView + +urlpatterns = [ + path("admin/", admin.site.urls), + path("admin/doc/", include("django.contrib.admindocs.urls")), + path( + "robots\.txt", + TemplateView.as_view(template_name="robots.txt", content_type="text/plain"), + ), +] + static( + getattr(settings, "MEDIA_URL", "/media/"), + document_root=getattr(settings, "MEDIA_ROOT", "/var/www/{{ project_name }}/media/"), +) + +if settings.DEBUG: + import debug_toolbar + + urlpatterns.insert( + 0, + path("__debug__/", include(debug_toolbar.urls)), + ) diff --git a/project_name/utils.py-tpl b/project_name/utils.py-tpl new file mode 100644 index 0000000..634f486 --- /dev/null +++ b/project_name/utils.py-tpl @@ -0,0 +1,7 @@ +"""global utils for {{ project_name }}""" + + +def str_to_bool(input_string: str) -> bool: + """ check string for truthiness """ + truthy_strings = ["true", "tru", "t", "y", "yes", "1"] + return input_string.lower() in truthy_strings diff --git a/project_name/wsgi.py-tpl b/project_name/wsgi.py-tpl new file mode 100644 index 0000000..0d68b95 --- /dev/null +++ b/project_name/wsgi.py-tpl @@ -0,0 +1,16 @@ +""" +WSGI config for {{ project_name }} project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "{{ project_name }}.settings") + +application = get_wsgi_application() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6c940d9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[tool.poetry] +name = "{{ project_name }}" +version = "0.1.0" +description = "" +authors = ["{% templatetag openvariable %} author_name {% templatetag closevariable %} <{% templatetag openvariable %} author_email {% templatetag closevariable %}>"] + +[tool.poetry.dependencies] +python = "^3.8" +Django = "^3.2.5" +django-extensions = "^3.1.3" +celery = "^5.1.2" +gunicorn = "^20.1.0" +Pillow = "^8.3.1" +docutils = "^0.16" +django-celery-beat = "^2.2.1" +django-celery-results = "^2.2.0" +psycopg2 = "^2.9.1" +pylibmc = "^1.6.1" + +[tool.poetry.dev-dependencies] +django-debug-toolbar = "^3.2.1" +ipython = "^7.25.0" +ipdb = "^0.13.9" +Werkzeug = "^1.0.1" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..e69de29 diff --git a/static/img/favicon.ico b/static/img/favicon.ico new file mode 100755 index 0000000..6881043 Binary files /dev/null and b/static/img/favicon.ico differ diff --git a/templates/robots.txt b/templates/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/templates/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /