Архив рубрики: Бизнес-информатика — это круто!

Как написать устанавливаемое приложение Django

В Django проект  - набор конфигурационных файлов и код для конкретного сайта. Django группирует бизнес-логику в так называемые приложения , которые являются модулями фреймворка Django. Существует множество документации о том, как структурировать ваши проекты и приложения в них, но когда приходит время упаковать устанавливаемое приложение Django, информацию найти сложнее.В этом руководстве вы узнаете, как извлечь приложение из проекта Django и упаковать его, чтобы его можно было установить. После того, как вы упаковали свое приложение, вы можете поделиться им в PyPI, чтобы другие могли получить его с помощью pip install .В этом руководстве вы узнаете:

  • В чем разница между написанием автономных приложений и написанием приложений внутри проектов
  • Как создать файл setup.cfg для публикации вашего приложения Django
  • Как загрузить Django вне проекта Django, чтобы вы могли протестировать свое приложение
  • Как протестировать несколько версий Python и Django с помощью tox
  • Как опубликовать устанавливаемое приложение Django в PyPI с помощью Twine

Даже если вы изначально намеревались сделать свое приложение Django доступным в виде пакета, вы, скорее всего, начнете внутри проекта. Чтобы продемонстрировать процесс перехода от проекта Django к устанавливаемому приложению Django, я сделал две ветки доступными в репозитории. Ветка проекта является начальным состоянием из приложения внутри проекта Django. Основная ветка — это готовое устанавливаемое приложение.

Вы также можете скачать готовое приложение на странице пакета PyPI realpython-django-чеков

Приложение в качестве примера - краткое представление позиций в квитанции. В ветке проекта вы найдете каталог с именем sample_project , содержащий рабочий проект Django. Каталог выглядит так:

sample_project/
│
├── receipts/
│   ├── fixtures/
│   │   └── receipts.json
│   │
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   │
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
│
├── sample_project/
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
│
├── db.sqlite3
├── manage.py
├── resetdb.sh
└── runserver.sh

Самая последняя версия Django на момент написания этого руководства была 3.0.4, и все тестирование проводилось на Python 3.7. Ни один из шагов, описанных в этом руководстве, не должен быть несовместим с более ранними версиями Django - я использовал эти методы, начиная с Django 1.8. Однако некоторые изменения необходимы, если вы используете Python 2. Чтобы не усложнять примеры, я использовал Python 3.7 для всей базы кода.

Создание проекта Django с нуля

Пример проекта и приложение квитанций были созданы с помощью команды администратора Django с небольшими изменениями. Для начала запустите следующий код в чистой виртуальной среде:

$ python -m pip install Django
$ django-admin startproject sample_project
$ cd sample_project
$ ./manage.py startapp receipts

Это создает структуру каталогов проекта sample_project и подкаталог приложения квитанций с файлами шаблонов, которые вы будете использовать для создания своего устанавливаемого приложения Django.

Затем в файл sample_project / settings.py необходимо внести несколько изменений:

  • Добавьте «127.0.0.1» в параметр ALLOWED_HOSTS, чтобы вы могли тестировать свой код локально.
  • Добавьте «receipts» в список INSTALLED_APPS .

Вам также необходимо зарегистрировать в Квитанции URL - приложения в sample_project / urls.py файл. Для этого добавьте путь (‘receipts/', include (receipts.urls')) в список url_patterns .

Знакомство с приложением-образцом квитанций

Приложение состоит из двух классов модели ORM: Item и Receipt . Класс Item содержит объявления полей базы данных для описания и стоимости. Стоимость содержится в DecimalField . Использование чисел с плавающей запятой для представления денег опасно - вы всегда должны использовать числа с фиксированной запятой при работе с валютами .

Класс Receipt - это точка сбора для объектов Item . Это достигается с помощью ForeignKey по пункту, который указывает на квитанции. Receipt также включает total () для получения общей стоимости объектов Item, содержащихся в Receipt:

# receipts/models.py
from decimal import Decimal
from django.db import models

class Receipt(models.Model):
    created = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"Receipt(id={self.id})"

    def total(self) -> Decimal:
        return sum(item.cost for item in self.item_set.all())

class Item(models.Model):
    created = models.DateTimeField(auto_now_add=True)

    description = models.TextField()
    cost = models.DecimalField(max_digits=7, decimal_places=2)
    receipt = models.ForeignKey(Receipt, on_delete=models.CASCADE)

    def __str__(self):
        return f"Item(id={self.id}, description={self.description}, " \
            f"cost={self.cost})"

Объекты модели предоставляют вам контент для базы данных. Короткое представление Django возвращает словарь JSON со всеми объектами Receipt и их объектами Item в базе данных:

# receipts/views.py
from django.http import JsonResponse
from receipts.models import Receipt

def receipt_json(request):
    results = {
        "receipts":[],
    }

    for receipt in Receipt.objects.all():
        line = [str(receipt), []]
        for item in receipt.item_set.all():
            line[1].append(str(item))

        results["receipts"].append(line)

    return JsonResponse(results)

Представление Receipt выполняет итерацию по всем объектам Receipt , создавая пару объектов Receipt и список содержащихся в них объектов Item . Все это помещается в словарь и возвращается через Django JsonResponse () .

Чтобы сделать модели доступной в интерфейсе администратора Django , вы используете файл admin.py для регистрации моделей:

# receipts/admin.py
from django.contrib import admin

from receipts.models import Receipt, Item

@admin.register(Receipt)
class ReceiptAdmin(admin.ModelAdmin):
    pass

@admin.register(Item)
class ItemAdmin(admin.ModelAdmin):
    pass

Этот код создает Django ModelAdmin для каждого из классов Receipt и Item и регистрирует их у администратора Django.

Наконец, файл urls.py регистрирует отдельное представление в приложении по URL-адресу:

# receipts/urls.py
from django.urls import path

from receipts import views

urlpatterns = [
    path("receipt_json/", views.receipt_json),
]

Теперь вы можете включать в receipts/ urls.py в вашего проекта url.py файл , чтобы сделать вид квитанции доступным на веб - сайте.

Когда все готово, вы можете запустить receipts​/manage.py makemigrations , использовать администратор Django для добавления данных, а затем посетить / Receipt / Receiver_json / для просмотра результатов:

$ curl -sS http://127.0.0.1:8000/receipts/receipt_json/ | python3.8 -m json.tool
{
    "receipts": [
        [
            "Receipt(id=1)",
            [
                "Item(id=1, description=wine, cost=15.25)",
                "Item(id=2, description=pasta, cost=22.30)"
            ]
        ],
        [
            "Receipt(id=2)",
            [
                "Item(id=3, description=beer, cost=8.50)",
                "Item(id=4, description=pizza, cost=12.80)"
            ]
        ]
    ]
}
 

В приведенном выше блоке вы используете curl для открытия представления receive_json , что возвращает JSON, содержащему объекты Receipt и соответствующие им объекты Item .

Тестирование приложения в проекте

Django дополняет пакет Python unittest своими собственными возможностями тестирования, позволяя предварительно загружать данные в базу данных и запускать тесты. Приложение receipts определяет tests.py файл и приспособление для тестирования. Этот тест ни в коем случае не является всеобъемлющим, но это достаточно хорошее доказательство концепции:

# receipts/tests.py
from decimal import Decimal
from django.test import TestCase
from receipts.models import Receipt

class ReceiptTest(TestCase):
    fixtures = ["receipts.json", ]

    def test_receipt(self):
        receipt = Receipt.objects.get(id=1)
        total = receipt.total()

        expected = Decimal("37.55")
        self.assertEqual(expected, total)

Приспособление создает два объекта « receipts» и четыре соответствующих объекта « Предмет» . Нажмите на складную секцию ниже, чтобы поближе познакомиться с кодом приспособления.

Вы можете проверить приложение квитанций с помощью команды Django manage.py :

$ ./manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.013s

OK
Destroying test database for alias 'default'...

При выполнении теста manage.py запускается единственный тест, определенный в файле receivets / tests.py, и отображаются результаты.

Создание устанавливаемого приложения Django

Ваша цель - поделиться приложением receipts без проекта и сделать его пригодным для повторного использования другими. Вы можете заархивировать каталог receipts/ и раздать его, но это несколько ограничивающее решение. Вместо этого вы можете разделить приложение на пакет, чтобы его можно было установить.

Самая большая проблема при создании устанавливаемого приложения Django заключается в том, что Django ожидает проект. Приложение без проекта - это просто каталог, содержащий код. Без проекта Django не знает, как что-либо делать с вашим кодом, включая выполнение тестов.

Удаление вашего приложения Django из проекта

Хорошая идея - сохранить образец проекта, чтобы вы могли запустить сервер Django dev и поиграть с действующей версией вашего приложения. Вы не будете включать этот образец проекта в пакет приложения, но он все равно может находиться в вашем репозитории. Следуя этой идее, вы можете начать упаковывать свое устанавливаемое приложение Django, переместив его в каталог вверх:

$ mv receipts ..

Структура каталогов теперь выглядит примерно так:

django-receipts/
│
├── receipts/
│   ├── fixtures/
│   │   └── receipts.json
│   │
│   ├── migrations/
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   │
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   ├── views.py
│   ├── admin.py
│   └── apps.py
│
├── sample_project/
│   ├── sample_project/
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   │
│   ├── db.sqlite3
│   ├── manage.py
│   ├── resetdb.sh
│   └── runserver.sh
│
├── LICENSE
└── README.rst

Чтобы упаковать приложение, вам нужно вытащить его из проекта. Перемещение - это первый шаг. Обычно я храню исходный проект для тестирования, но не включаю его в итоговый пакет.

Загрузка Django вне проекта

Теперь, когда ваше приложение находится за пределами проекта Django, вам нужно указать Django, как его найти. Если вы хотите протестировать свое приложение, запустите оболочку Django, которая сможет найти ваше приложение или выполнить миграции . Вам нужно будет настроить Django и сделать его доступным.

Django settings.configure () и django.setup () являются ключевыми для взаимодействия с вашим приложением вне проекта. Дополнительная информация об этих вызовах доступна в документации Django .

Скорее всего, вам понадобится эта конфигурация Django в нескольких местах, поэтому имеет смысл определить ее в функции. Создайте файл с именем boot_django.py, содержащий следующий код:

 1# boot_django.py
 2#
 3# This file sets up and configures Django. It's used by scripts that need to
 4# execute as if running in a Django server.
 5import os
 6import django
 7from django.conf import settings
 8
 9BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "receipts"))
10
11def boot_django():
12    settings.configure(
13        BASE_DIR=BASE_DIR,
14        DEBUG=True,
15        DATABASES={
16            "default":{
17                "ENGINE":"django.db.backends.sqlite3",
18                "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
19            }
20        },
21        INSTALLED_APPS=(
22            "receipts",
23        ),
24        TIME_ZONE="UTC",
25        USE_TZ=True,
26    )
27    django.setup()

 

Строки 12 и 27 устанавливают среду Django. Settings.configure () вызов принимает список аргументов, которые эквивалентны переменным, определенным в settings.py файла. Все, что вам может понадобиться в вашем settings.py для запуска вашего приложения, передается в settings.configure () .

Приведенный выше код представляет собой довольно урезанную конфигурацию. Приложение receipts ничего не делает с сеансами или шаблонами, поэтому для INSTALLED_APPS нужны только «receipts» , и вы можете пропустить любые определения промежуточного программного обеспечения. Значение USE_TZ = True необходимо, потому что модель получения содержит созданную метку времени. В противном случае вы столкнетесь с проблемами при загрузке тестового устройства.

Запуск команд управления с помощью устанавливаемого приложения Django

Теперь, когда у вас есть boot_django.py , вы можете запустить любую команду управления Django с помощью очень короткого скрипта:

#!/usr/bin/env python
# makemigrations.py

from django.core.management import call_command
from boot_django import boot_django

boot_django()
call_command("makemigrations", "receipts")

Django позволяет программно вызывать команды управления через call_command () . Теперь вы можете запустить любую команду управления, импортировав и вызвав boot_django (), а затем call_command () .

Ваше приложение теперь находится за пределами проекта, что позволяет вам делать с ним все, что угодно Django. Я часто определяю четыре служебных сценария:

  1. load_tests.py для тестирования вашего приложения
  2. makemigrations.py для создания файлов миграции
  3. migrate.py для выполнения миграции таблиц
  4. djangoshell.py для создания оболочки Django, которая знает о вашем приложении.

Тестирование устанавливаемого приложения Django

Load_test.py файл может быть столь же простым, как makemigrations.py, но тогда была бы единственная возможность запустить все тесты сразу. С помощью нескольких дополнительных строк вы можете передать аргументы командной строки средству выполнения тестов, что позволяет запускать выборочные тесты:

 1#!/usr/bin/env python
 2# load_tests.py
 3import sys
 4from unittest import TestSuite
 5from boot_django import boot_django
 6
 7boot_django()
 8
 9default_labels = ["receipts.tests", ]
10
11def get_suite(labels=default_labels):
12    from django.test.runner import DiscoverRunner
13    runner = DiscoverRunner(verbosity=1)
14    failures = runner.run_tests(labels)
15    if failures:
16        sys.exit(failures)
17
18    # In case this is called from setuptools, return a test suite
19    return TestSuite()
20
21if __name__ == "__main__":
22    labels = default_labels
23    if len(sys.argv[1:]) > 0:
24        labels = sys.argv[1:]
25
26    get_suite(labels)

DiscoverRunner от Django — это класс обнаружения тестов, совместимый с модулем тестирования Python. Он отвечает за настройку тестовой среды, построение набора тестов, настройку баз данных, запуск тестов, а затем завершение. Начиная со строки 11, get_suite () берет список тестовых меток и напрямую вызывает для них DiscoverRunner.

Этот сценарий похож на то, что делает тест команды управления Django. Блок __main__ передает любые аргументы командной строки в get_suite (), и если их нет, то он передает набор тестов для приложения Receiver.tests. Теперь вы можете вызвать load_tests.py с аргументом тестовой метки и запустить один тест.

Строка 19 — это особый случай, помогающий при тестировании с помощью tox. Вы узнаете больше о tox в следующем разделе . Вы также можете проверить потенциальную замену DiscoverRunner в сворачиваемом разделе ниже.

Определение устанавливаемого пакета с помощью setup.cfg

Чтобы разместить устанавливаемое приложение Django на PyPI, вам нужно сначала поместить его в пакет. PyPI ожидает распространения в виде файлов с исходным кодом. Они создаются с использованием инструментов настройки. Для этого вам необходимо создать файлы setup.cfg и setup.py на том же уровне каталога, что и каталог receipts.

Однако, прежде чем углубляться в это, вы должны убедиться, что у вас есть некоторая документация. Вы можете включить описание проекта в setup.cfg , которое автоматически отображается на странице проекта PyPI. Обязательно напишите README.rst или что-то подобное с информацией о вашем пакете.

PyPI по умолчанию поддерживает формат reStructuredText , но он также может обрабатывать Markdown с дополнительными параметрами :

 1# setup.cfg
 2[metadata]
 3name = realpython-django-receipts
 4version = 1.0.3
 5description = Sample installable django app
 6long_description = file:README.rst
 7url = https://github.com/realpython/django-receipts
 8license = MIT
 9classifiers =
10    Development Status :: 4 - Beta
11    Environment :: Web Environment
12    Intended Audience :: Developers
13    License :: OSI Approved :: MIT License
14    Operating System :: OS Independent
15    Programming Language :: Python :: 3 :: Only
16    Programming Language :: Python :: 3.7
17    Programming Language :: Python :: Implementation :: CPython
18    Topic :: Software Development :: Libraries :: Application Frameworks
19    Topic :: Software Development :: Libraries :: Python Modules
20
21[options]
22include_package_data = true
23python_requires = >=3.6
24setup_requires =
25    setuptools >= 38.3.0
26install_requires =
27    Django>=2.2

setup.cfg описывает пакет, который вы создадите. Строка 6 использует директиву file: для чтения в вашем файле README.rst. Это избавляет вас от необходимости писать длинное описание в двух местах.

Запись install_requires в строке 26 сообщает любым установщикам, таким как pip install , о зависимостях вашего приложения. Вы всегда захотите привязать свое устанавливаемое приложение Django к его минимально поддерживаемой версии Django.

Если в вашем коде есть зависимости, которые необходимы только для запуска тестов, вы можете добавить запись tests_require = . Например, до того, как mock стал частью стандартной библиотеки Python, обычно можно было увидеть tests_require = mock> = 2.0.0 в setup.cfg .

Рекомендуется включать в пакет файл pyproject.toml . Прекрасная статья Бретта Кэннона на эту тему подробно расскажет об этом. Pyproject.toml файл также включен образец кода.

Вы почти готовы создать пакет для устанавливаемого приложения Django. Самый простой способ проверить это - использовать ваш образец проекта - еще одна веская причина сохранить образец проекта под рукой. Команда pip install поддерживает локально определенные пакеты. Это можно использовать, чтобы убедиться, что ваше приложение по-прежнему работает с проектом. Однако есть одно предостережение: в этом случае setup.cfg не будет работать сам по себе. Вам также необходимо создать версию с прокладкой для setup.py :

#!/usr/bin/env python

if __name__ == "__main__":
    import setuptools
    setuptools.setup()

Этот сценарий автоматически использует ваш файл setup.cfg . Теперь вы можете установить локальную редактируемую версию пакета, чтобы протестировать ее из sample_project . Чтобы быть уверенным вдвойне, лучше всего начать с совершенно новой виртуальной среды. Добавьте следующий requirements.txt файл внутри sample_project каталога:

# requirements.txt
-e ../../django-receipts

«-e» говорит pip, что это локальная редактируемая установка. Теперь вы готовы к установке:

$ pip install -r requirements.txt
Obtaining django-receipts (from -r requirements.txt (line 1))
Collecting Django>=3.0
  Using cached Django-3.0.4-py3-none-any.whl (7.5 MB)
Collecting asgiref~=3.2
  Using cached asgiref-3.2.7-py2.py3-none-any.whl (19 kB)
Collecting pytz
  Using cached pytz-2019.3-py2.py3-none-any.whl (509 kB)
Collecting sqlparse>=0.2.2
  Using cached sqlparse-0.3.1-py2.py3-none-any.whl (40 kB)
Installing collected packages: asgiref, pytz, sqlparse, Django, realpython-django-receipts
  Running setup.py develop for realpython-django-receipts
Successfully installed Django-3.0.4 asgiref-3.2.7 pytz-2019.3 realpython-django-receipts sqlparse-0.3.1

Список install_requires в setup.cfg сообщает pip install, что ему нужен Django. Django нуждается asgiref , pytz и sqlparse . Все зависимости учтены, и теперь вы можете запустить свой сервер sample_project Django dev. Поздравляем - теперь ваше приложение упаковано, и на него есть ссылки в образце проекта!

Тестирование нескольких версий с помощью tox

Django и Python постоянно развиваются. Если вы собираетесь поделиться своим устанавливаемым приложением Django со всем миром, вам, вероятно, придется протестировать его в нескольких средах . Инструменту tox требуется небольшая помощь, чтобы иметь возможность протестировать ваше приложение Django. Сделайте следующее изменение внутри setup.cfg :

 1# setup.cfg
 2[metadata]
 3name = realpython-django-receipts
 4version = 1.0.3
 5description = Sample installable django app
 6long_description = file:README.rst
 7url = https://github.com/realpython/django-receipts
 8license = MIT
 9classifiers =
10    Development Status :: 4 - Beta
11    Environment :: Web Environment
12    Intended Audience :: Developers
13    License :: OSI Approved :: MIT License
14    Operating System :: OS Independent
15    Programming Language :: Python :: 3 :: Only
16    Programming Language :: Python :: 3.7
17    Programming Language :: Python :: Implementation :: CPython
18    Topic :: Software Development :: Libraries :: Application Frameworks
19    Topic :: Software Development :: Libraries :: Python Modules
20
21[options]
22include_package_data = true
23python_requires = >=3.6
24setup_requires =
25    setuptools >= 38.3.0
26install_requires =
27    Django>=2.2
28test_suite = load_tests.get_suite

Строка 28 сообщает менеджеру пакетов использовать сценарий load_tests.py для получения своего набора тестов.”tox” использует это, чтобы запустить испытания. Вспомните get_suite () в load_tests.py:

 1# Defined inside load_tests.py
 2def get_suite(labels=default_labels):
 3    from django.test.runner import DiscoverRunner
 4    runner = DiscoverRunner(verbosity=1)
 5    failures = runner.run_tests(labels)
 6    if failures:
 7        sys.exit(failures)
 8
 9    # If this is called from setuptools, then return a test suite
10    return TestSuite()

По общему признанию, то, что здесь происходит, немного странно. Обычно поле test_suite в setup.cfg указывает на метод, который возвращает набор тестов. Когда tox вызывает setup.py , он считывает параметр test_suite и запускает load_tests.get_suite () .

Если этот вызов не вернул объект TestSuite , то tox пожаловался бы. Странная часть заключается в том, что нам не нужно, чтобы tox получал набор тестов, потому что tox не знает о тестовой среде Django. Вместо этого get_suite () создает DiscoverRunner и возвращает пустой объект TestSuite в строке 10.

Вы не можете просто заставить DiscoverRunner вернуть набор тестов, потому что вам нужно вызвать DiscoverRunner.run_tests () для правильного выполнения настройки и разборки тестовой среды Django. Простая передача правильных тестов в tox не сработает, потому что база данных не будет создана. get_suite () запускает все тесты, но как побочный эффект вызова функции, а не как обычный случай возврата набора тестов для выполнения tox .

Инструмент tox позволяет тестировать несколько комбинаций. Tox.ini файл определяет , какие комбинации сред для тестирования. Вот пример:

[tox]
envlist = py{36,37}-django220, py{36,37}-django300

[testenv]
deps =
    django220: Django>=2.2,<3
    django300: Django>=3
commands=
    python setup.py test

 

$ python -m pip install -U wheel twine setuptools
$ python setup.py sdist
$ python setup.py bdist_wheel
$ twine upload dist/*

Первые две команды создают исходный и двоичный дистрибутивы вашего пакета. Вызов шпагата загружается в PyPI. Если у вас есть файл .pypirc в вашем домашнем каталоге, вы можете предварительно установить свое имя пользователя, поэтому единственное, что вам будет предложено, это ваш пароль:

[disutils]
index-servers =
    pypi

[pypi]
username: <YOUR_USERNAME>
Я часто использую небольшой скрипт для Grep номера версии из кода. Затем я вызываю тег git, чтобы пометить репозиторий номером версии, удалить старые каталоги build / и dist / и вызвать указанные выше три команды.Дополнительные сведения об использовании Twine см. В разделе « Как опубликовать пакет Python с открытым исходным кодом в PyPI» . Две популярные альтернативы Twine - Poetry и Flit . Управление пакетами в Python быстро меняется. PEP 517 и PEP 518 переопределяют способ описания пакетов и зависимостей Python.

Заключение

Приложения Django полагаются на структуру проекта Django, поэтому их упаковка по отдельности требует дополнительных действий. Вы видели, как создать устанавливаемое приложение Django, извлекая его из проекта, упаковывая и публикуя в PyPI. Обязательно загрузите образец кода по ссылке ниже:

В этом уроке вы узнали, как:

  • Использовать фреймворк Django вне проекта
  • Вызывать команды управления Django в приложении, не зависящем от проекта
  • Написать скрипт, который вызывает тесты Django , при желании используя одну тестовую метку.
  • Создать файл setup.py для определения вашего пакета
  • Изменить скрипт setup.py для работы с tox
  • Использовать Twine для загрузки устанавливаемого приложения Django

Вы готовы поделиться своим следующим приложением со всем миром. Удачного кодирования!

Что изучать дальше?

Django, упаковка и тестирование - это очень глубокие темы. Там много информации. Чтобы копнуть глубже, ознакомьтесь со следующими ресурсами:

PyPI имеет множество устанавливаемых приложений Django, которые стоит попробовать. Вот некоторые из самых популярных:

Ботсвана - Обзор рынка

Ботсвана - Обзор рынка

Ботсвана предлагает стабильную политическую, фискальную и макроэкономическую среду. ВВП Ботсваны на душу населения составляет 7 595 долл. США (82 110 пул) по данным Всемирного банка на 2017 год, что делает ее страной с уровнем доходов выше среднего в соответствии со стандартами Всемирного банка. ВВП на 2017 год был оценен Всемирным банком примерно в 17,48 млрд. Долларов (189 млрд. Фунтов стерлингов). Банк Ботсваны сообщает, что в 2018 году ВВП составил 18,35 миллиарда долларов (198 миллиардов фунтов).

Читать далее Ботсвана - Обзор рынка