Share This
Связаться со мной
Крути в низ
Categories
//🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

В руководстве подробно рассматривается пример использования Python-библиотеки облачного представления приложений Streamlit и системы компьютерной вёрстки LaTeX для подготовки сложных аналитических отчетов с математическими, программными и графическими вставками. Обсудить

slozhnostrukturnye analiticheskie otchety s python i latex f37493a - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Экосистема LaTeX. Быстрый старт

  • TeX (произносится как «тех») – система компьютерной верстки, предназначенная для подготовки научно-технических материалов высокого полиграфического качества.
  • LaTeX (произносится «латех») – это, строго говоря, набор макросов на языке разметки TeX, но в зависимости от контекста под LaTeX может пониматься макропакет, издательская система или язык, служащий для разметки документа. LaTeX 2e – наиболее полная, стабильная версия LaTeX.
  • MikTеX – свободно распространяемая реализация TeX под основные операционные системы – Windows, macOS, Linux (Ubuntu, Debian, CentOS и пр.) – включающая в себя практически все наиболее значимые расширения.

Установить реализацию MikTеX под нужную операционную систему можно по инструкциям на странице проекта. При желании с системой можно взаимодействовать и без установки дистрибутива, просто запустив Docker-образ:

miktex_install.sh

         $ docker pull miktex/miktex $ docker volume create --name miktex $ docker run -it    -v miktex:/miktex/.miktex    -v $(pwd):/miktex/work    miktex/miktex    pdflatex main.tex     

Здесь с помощью команды docker pull в локальное хранилище образов скачивается Docker-образ miktex/miktex. Затем командой docker volume в файловой системе хоста создается директория miktex/. Последним шагом с помощью команды docker run остается запустить контейнер Docker на базе образа miktex/miktex. Флаг -it создает сеанс интерактивной работы на подключаемом терминальном устройстве, а флаг -v отображает директорию файловой системы хоста на директорию внутри контейнера.

Контейнер ожидает получить аргументы командной строки указывающие на выбранный компилятор (в данном случае PDFLaTeX) и собственно tex-файл (размеченный документ, на основании которого позже будет создан pdf-файл).

В качестве альтернативы MikTEX можно использовать TeX Live – это еще один свободно распространяемый и наиболее полный дистрибутив TeX. Для операционной системы MacOS X можно пользоваться специализированным решением MacTeX.

Опорный документ LaTeX (tex-файл) представляет собой обычный текстовый файл с разметкой. Для редактирования такого рода файлов можно пользоваться и обычным текстовым Unix-редактором Vim, но практичнее использовать специальные решения, например, открытую среду разработки TeXstudio (поддерживает все основные операционные системы). Однако TeXstudio не единственный вариант. Вот наиболее распространенные альтернативы: TeXmaker, Kile, TeXCenter.

LaTeX состоит из пакетов, получить доступ к которым можно на странице CTAN (The Comprehensive TeX Archive Network). CTAN – центральный репозиторий, в который стекаются все сколько-нибудь стоящие наработки, имеющие отношение к TeX.

Каждый пакет репозитория содержит документацию (с описанием зависимостей, примерами и контекстом использования, деталями реализации и пр.), исходные файлы, описание синтаксических особенностей, макродемонстрацию и другие компоненты, позволяющие быстро понять устройство пакета и порядок работы с ним. С общей структурой пакетов CTAN можно познакомиться на примере пакета для оформления программного кода listings.

Итак, для запуска примеров из настоящего руководства потребуется установить дистрибутив TeX (например, MikTеX) и LaTeX-редактор (например, TeXstudio).

Введение в язык разметки LaTeX

В текстовых процессорах, которые позволяют в момент набора текста видеть его на экране дисплея точно таким, как он будет выглядеть на бумаге, используется концепция визуального проектирования (WYSIWYG – What You See Is What You Get). В LaTeX же используется концепция логического проектирования, когда внешний вид документа становится понятен только после компиляции.

Внешний вид документа определяется инструкциями, описанными на языке разметки TeX или LaTeX. Между этими языками сложно провести четкую границу. Сам LaTeX написан в командах TeX и в LaTeX-документе можно использовать практические любые TeX-команды. Упрощенно можно считать, что язык разметки LaTeX это высокоуровневая «синтаксически-сахарная» обертка для низкоуровневого языка TeX. Таким образом, TeX в основном применяется при разработке классов и пакетов, в то время как LaTeX – для подготовки печатного документа.

Теперь рассмотрим общий синтаксис языка LaTeX и познакомимся с базовой структурой tex-документа.

  • LaTeX-файл должен начинаться с команды documentclass, задающей стиль оформления документа, например, documentclass{article}. Аргумент команды article означает, что документ будет оформлен в соответствии с наиболее общими правилами оформления статей. При желании можно изменить (с незначительными оговорками) любой элемент макета документа.
  • Команда documentclass поддерживает и другие классы, доступные «из коробки»: book (для оформления книг), report (для оформления отчетов – нечто среднее между book и article), proc (для оформления трудов конференций) и letter (для деловых писем со сложной структурой).
  • После команды documentclass могут следовать команды, относящиеся ко всему документу. Далее с помощью окруженияbegin{document}end{document} (условимся называть его документарным окружением) указывается тело документа. В общем случае окружением называют сложные конструкции вида begin{}end{}.
  • Часть tex-файла между командой documentclass и begin{document}end{document} называют преамбулой. Команды указанные после закрывающей скобки окружения документа end{document} LaTeX проигнорирует.

В итоге наипростейший шаблон документа будет выглядеть так:

base_example.tex

         documentclass{article} % область преамбулы begin{document} % тело документа: начало % текст end{document} % тело документа: конец % команды в этой части документа не будут учитываться при сборке документа     

Инструкции, управляющие макетом страницы и прочие настройки tex-документа, могут быть размещены и в преамбуле, но когда инструкций становится много, будет удобнее вынести их в отдельный стилевой файл с расширением *.sty. Позже стилевой файл можно будет подключить командой usepackage{}, принимающей в качестве аргумента путь до этого файла.

Сам стилевой файл содержит импорты пакетов RequirePackage{}, пользовательские команды newcommand{}, псевдонимы математических операторов DeclareMathOperator и прочие элементы кастомизации:

style_template.sty

         % начало стилевого файла RequirePackage[english,russian]{babel} RequirePackage[utf8]{inputenc} RequirePackage{amsmath, amsfonts, amssymb, latexsym} RequirePackage[ 	left=2cm, 	right=2cm, 	top=2cm, 	bottom=2cm 		]{geometry} ...  newcommand{str}[1]{cтр.~pageref{#1}} newcommand{strbook}[1]{стр.~{#1}} ... DeclareMathOperator*{argmin}{arg,min} DeclareMathOperator*{argmax}{arg,max} DeclareMathOperator*{sign}{sign} DeclareMathOperator*{const}{const} ...     

С подключенным стилевым файлом tex-документ будет выглядеть так:

base_example_with_usepackage.tex

         documentclass{article} usepackage{style_template} % подключаем стилевой файл style_template.sty, расположенный в той же директории, что и tex-файл begin{document} % текст end{document}     

Аргумент команды usepackage может включать не только имя стилевого файла, но и путь до него относительно корня проекта (без указания расширения файла).

Далее предполагается, что все настройки макета документа описаны в стилевом файле style_template.sty, который расположен в той же директории, что и tex-файл.

К сожалению, в настоящем руководстве нет возможности рассмотреть все элементы и тонкости подготовки LaTeX-файлов, но в github-репозитории доступен полный код рассматриваемого примера с пояснениями, включая опорный стилевой файл, адаптированный для работы с кириллицей и содержащий декоративные настройки окружений, которые могут пригодиться при подготовке сложноструктурных аналитических отчетов.

Разобравшись с базовыми структурными элементами tex-файла, можно сосредоточиться на содержательной части и перейти к наполнению документа.

Наберем в документарном окружении следующие строки:

simple_onepage_template.tex

         ... begin{document}  title{Аналитический отчет по ...} % заголовок отчета author{itshape Иванов И.И.} % автор работы date{} % просим LaTeX не указывать дату, так как будет         % использован наш вариант оформления даты, описанный в стилевом файле maketitle % создает заголовок  thispagestyle{fancy} % задает стиль страницы  В этой части можно разместить аннотацию к отчету. TeX -- это издательская система компьютерной верстки, предназначенная для набора ...  tableofcontents % создает оглавление  section{Пример многострочной формулы}     Для набора сложных многострочных формул используются различные окружения, например, окружение texttt{multline} 	begin{multline} 		F_{zeta}(z)=P[,zetaleqslant z,] = int!!!int_{x/yleqslant z}f_X(x;n)f_Y(y;m),dxdy =\ dfrac{1}{2^{(n+m)/2}Gamma(n/2)Gamma(m/2)}int!!!int_{x/yleqslant z}x^{n/2-1}y^{m/2-1}expleft( -frac{x}{2} right) expleft( -frac{y}{2} right) ,mathrm{d}x , mathrm{d}y. 	end{multline}  section{Пример группового размещения формул}  Несколько формул можно разместить в одной группе с помощью окружения texttt{gather} begin{gather} 	sum_{j in mathbf{N}} b_{ij} hat{y}_{j} = sum_{j in mathbf{N}} b_{ij}^lambda hat{y}_j + (b_{ii} - lambda_i)hat{y}_i hat{y},notag \ 	det mathbf{K}(t=1, t_1, ldots, t_n) = sum_{I in mathbf{n} } (-1)^{|I|} prod_{i in I} t_i prod_{j in I} (D_j + lambda_j t_j) det mathbf{A}^{(lambda)} (, overline{I} | overline{I} ,) = 0,tag{$a$} \ 	mathbb{F} = sum_{i=1}^{left[ frac{n}{2}right] } binom{ x_{i,i+1}^{i^2}}{ left[ frac{i+3}{3} right]} {{sqrt{mu(i)^frac{3}{2} (i^2-1)}} overdisplaystyle {sqrt[3]{rho(i)-2} + sqrt[3]{rho(i)-1}} }, tag{$b$} end{gather}  section{Простая однострочная формула}  Теорема Хинчина-Винера утверждает, что спектральная плотность мощности стационарного в широком смысле случайного процесса представляет собой преобразование Фурье от соответствующей автокорреляционной функции begin{equation*} % без нумерации     S_{xx}(f) = intlimits_{-infty}^{infty}  , r_{xx} (tau) e^{-j 2 pi f tau} mathrm{d} tau, text{где} r_{xx}(tau) = mathbb{E}[,x(t) , x^{*}(t - tau),]. end{equation*}  end{document}     

Теперь можно вызвать компилятор PDFLaTeX через командную оболочку и передать ему имя tex-файла. Результат компиляции приведен на рисунке ниже.

slozhnostrukturnye analiticheskie otchety s python i latex 59c5c55 - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Результат компиляции файла analyt_report_template.tex

Рассмотрим более подробно код файла analyt_report_template.tex.

  • В первых строках файла с помощью команд title и author мы объявляем заголовок отчета и имя автора работы соответственно, а с помощью команды maketitleсоздаем заголовок.
  • Затем с помощью команды date, вызванной без аргументов, подавляем вывод временной метки. Здесь для привязки ко времени будет использоваться специальная низкоуровневая командная вставка в стилевом файле style_template.sty (этот фрагмент кода приводится только для справки, так как конечному пользователю приложения нет необходимости вносить в стилевой файл изменения напрямую):

fragment_style_template.sty

         ...  def@maketitle{     begin{flushright} 	footnotesizeitshape 	Дата последней сборки документа:\ today в currenttime     end{flushright}      begin{center} 	let footnote thanks 	begin{spacing}{1.5} 	    {Largebfseries@title} 	end{spacing}vskip 1mm  	{normalsize 	    begin{tabular}[t]{l} 		@author 	    end{tabular}par	 	}     end{center}     par     vskip 1.5em } ...     

Команда thispagestyle задает стиль страницы, а команда tableofcontents создает оглавление документа. С помощью команды section создаются разделы документа высшего уровня (с учетом класса документа). Для того чтобы создать раздел более низкого уровня следующей ступени можно воспользоваться командой subsection.

LaTeX обладает широким набором окружений для оформления математических конструкций произвольной сложности, однако здесь мы ограничимся рассмотрением только трех типов окружения: equation, multline и gather.

Как должно быть понятно из приведенного выше рисунка, equation используется для однострочных формул, multline – для многострочных, а gather – для группового размещения формул. Звездочка после имени окружения означает, что LaTeX не станет присваивать номера формулам, попавшим в это окружение.

При наборе формул используются специальные LaTeX-команды с названиями созвучными набираемому элементу, например, для того чтобы добавить в формулу знак суммы с нижним lowindex и верхним upindex индексом используется команда sum_{lowindex}^{upindex}, чтобы вставить символ большой омеги – Omega, а для вставки символа гамма потребуется gamma и т.д.

Для быстрого поиска нужных математических LaTeX-команд, специальных символов, приемов оформления псевдокода и пр. удобно пользоваться компактным, но очень содержательным сборником Константина Воронцова «LaTeX 2e в примерах».

Прочие детали работы с издательской системой LaTeX можно выяснить из следующих работ:

  • Дональд Кнут «Все про TeX»: фундаментальное руководство, которое включает подробное обсуждение множества низкоуровневых технических моментов;
  • Сергей Львовский «Набор и верстка в пакете LaTeX»;
  • Евгений Балдин «Компьютерная типография LaTeX».

Введение в Python-библиотеку Streamlit

Библиотека Streamlit – это простая, лаконичная и в тоже время очень мощная библиотека для прототипирования браузерных решений с графическим интерфейсом. Streamlit включает поддержку всех основных элементов стека, ориентированных на компьютерное зрение, машинное и глубокое обучение.

Установить библиотеку проще всего с помощью менеджера пакетов pip: pip install streamlit.

Запускается приложение командой streamlit run:

streamlit_run.sh

         $ streamlit run streamlit_simple_example.py    You can now view your Streamlit app in your browser.    Local URL: http://localhost:8502 # <--    Network URL: http://192.168.1.247:8502     

После запуска сценария в браузере (http://localhost:8502) откроется вкладка с приложением. Завершить работу приложения можно, закрыв вкладку браузера и набрав Ctrl+C в командной оболочке.

В Streamlit реализован сравнительно небольшой набор «выразительных средств», но все элементы продуманы и покрывают значительную часть требований к «гибкому динамическому прототипу».

Начать работу со Streamlit можно с вводного руководства, которое подробно описывает все возможности библиотеки, включая Markdown-разметку, аудио и видео объекты, различные графические объекты (Bokeh, Altair, Plotly и др.), а также оптимизацию загрузки объемных наборов данных.

Ниже приводится пример использования библиотеки Streamlit для построения двух интерактивных графиков (на базе библиотеки Plotly) гауссовских процессов с автокорреляционной функцией экспоненциального типа.

streamlit_simple_example.py

         import streamlit as st import math import pandas as pd from pandas import Series import plotly.graph_objects as go import numpy as np import numpy.random as rnd  title_app = "Простой пример использования библиотеки Streamlit" # чтобы на вкладке браузера отображалось имя приложения, а не имя файла st.set_page_config(     layout="wide",     page_title=title_app,     initial_sidebar_state="expanded", )   def gauss_with_exp_acf_gen(     *,     sigma: float = 2,     w_star: float = 1.25,     delta_t: float = 0.05,     N: int = 1000, ) -> np.array:     """     Описание     --------     Генерирует дискретную реализацию     стационарного гауссовского ПСП     с КФ экспоненциального типа      Параметры     ---------     sigma : стандартное отклонение ординат ПСП.     w_star : параметр модели ПСП.     delta_t : шаг по времени.     N : число отсчетов ПСП.      Возвращает     ----------     xi : массив элементов ПСП с заданной КФ     """     gamma_star = w_star * delta_t     rho = math.exp(-gamma_star)     b1 = rho     a0 = sigma * math.sqrt(1 - rho ** 2)      xi = np.zeros(N)     xi[0] = rnd.rand()     x = rnd.randn(N)      for n in range(1, N):         xi[n] = a0 * x[n] + b1 * xi[n - 1]      return xi   def main(N: int = 100):     timestmp = np.arange(N)     # временной ряд №1     time_series_1 = gauss_with_exp_acf_gen(sigma=5, w_star=1.25, N=N)     # временной ряд №2     time_series_2 = gauss_with_exp_acf_gen(sigma=6.5, w_star=1.75, N=N)      fig = go.Figure()      fig.add_trace(         go.Scatter(             x=timestmp,             y=time_series_1,             name="Временной ряд (объект-1)",             opacity=0.8,             mode="lines",             line=dict(                 color="#E84A5F",             ),         )     )      fig.add_trace(         go.Scatter(             x=timestmp,             y=Series(time_series_1).rolling(window=7).mean(),             name="Скользящее среднее (объект-1)",             mode="lines",             opacity=0.6,             line=dict(                 color="#FF847C",             ),         )     )      fig.add_trace(         go.Scatter(             x=timestmp,             y=time_series_2,             name="Временной ряд (объект-2)",             mode="lines",             opacity=0.8,             line=dict(                 color="#5E63B6",             ),         )     )      fig.add_trace(         go.Scatter(             x=timestmp,             y=Series(time_series_2).rolling(window=7).mean(),             name="Скользящее среднее (объект-2)",             mode="lines",             opacity=0.6,             line=dict(                 color="#6EB6FF",             ),         )     )      fig.update_layout(         title=dict(             # text="<i>Временной ряд</i>",             font=dict(                 family="Arial",                 size=18,                 color="#07689F",             ),         ),         xaxis_title=dict(             text="<i>Временная метка</i>",             font=dict(                 family="Arial",                 size=13,                 color="#537791",             ),         ),         yaxis_title=dict(             text="<i>Продолжительность простоя, час</i>",             font=dict(                 family="Arial",                 size=13,                 color="#537791",             ),         ),         xaxis=dict(             showline=True,         ),         yaxis=dict(             showline=True,         ),         autosize=False,         showlegend=True,         margin=dict(             autoexpand=False,             l=70,             r=10,             t=50,         ),         legend=dict(             orientation="v",             yanchor="bottom",             y=0.01,             xanchor="right",             x=0.99,             font=dict(family="Arial", size=12, color="black"),         ),         plot_bgcolor="white",     )      st.plotly_chart(fig, use_container_width=True)   if __name__ == "__main__":     main(N=350)     

В этом примере к библиотеке Streamlit имеет отношение только:

  • функция st.set_page_config(), которая переопределяет имя вкладки браузера – отображается не имя файла, а имя приложения;
  • функция st.plotly_chart(), которая принимает сконфигурированный Plotly-объект и выводит его в окно браузера.

slozhnostrukturnye analiticheskie otchety s python i latex c2a69ed - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Результат работы сценария streamlit_simple_example.py

Прочие элементы сценария streamlit_simple_example.py играют лишь вспомогательную роль.

Пример-шаблон аналитического отчета

Предлагается в качестве примера, сочетающего приемы работы с библиотекой Streamlit (браузерный интерфейс приложения) и LaTeX-разметку (опорный tex-файл), рассмотреть подготовку отчета на тему «Оценка усталостной долговечности силовых элементов транспортных машин под воздействием стационарных гауссовских процессов с автокорреляционной функцией экспоненциально-косинусного семейства».

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

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

Часто возникает необходимость выгрузить подготовленный с помощью Streamlit файл результатов (например, табличных объект Pandas), но сам Streamlit не предлагает никаких решений. Тем не менее это ограничение можно обойти с помощью следующей функции:

downloader_for_streamlit.py

         def text_downloader(multiline: str) -> NoReturn:     """     Принимает LaTeX-шаблон документа в виде многострочной строки и     создает на странице ссылку для скачивания шаблона     """     OUTPUT_TEX_FILENAME = "base_template_for_latex.tex"          b64 = base64.b64encode(multiline.encode()).decode()     # создает ссылку для скачивания файла     href = (f'<a href="data:file/txt;base64,{b64}" '             f'download="{OUTPUT_TEX_FILENAME}">Скачать ...</a>')     st.markdown(href, unsafe_allow_html=True)     

Функция читает текстовый файл, переданный в виде строки, кодирует его с помощью стандарта base64 и передает тег-ссылку методу markdown. В итоге файл можно будет скачать, кликнув на созданную ссылку.

При наборе текстового шаблона для Python (latex_template_for_python.txt) – по сути файл представляет собой каркас документа с заглушками под строковую интерполяцию – важно помнить о синтаксических особенностях LaTeX. Дело в том, что в контексте строковой интерполяции фигурные скобки означают «место подстановки», а в контексте LaTeX – обязательный аргумент команды. То есть, чтобы Python корректно «прочитал» строку следует фигурные скобки, относящиеся к синтаксису LaTeX, удвоить.

В результате получится текстовый файл вида:

fragment_latex_template_for_python.txt

         documentclass[     11pt,     a4paper,     utf8, ]{{article}}  usepackage{{style_template}}  begin{{document}} ... Усталостная долговечность по модели eqref{{eq:miles}} составляет $ Y_{{NB}} = {Y_NB:.2f} $, сек.  section{{Оценка усталостной долговечности по модели P.H. Wirsching и C.L.~Light}} ...     

Ниже на рисунках приводится общий вид приложения.

slozhnostrukturnye analiticheskie otchety s python i latex b04e8c1 - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Стартовая страница приложения

Скачав подготовленный tex-файл в директорию проекта, останется только запустить компилятор (дважды!).

pdflatex_start.sh

         # для сборки каркаса $ pdflatex base_template_for_latex.tex # для вычисления конечных ссылок на страницы, формулы и пр. $ pdflatex base_template_for_latex.tex      

После сборки документа в рабочей директории проекта будет создан pdf-файл.

slozhnostrukturnye analiticheskie otchety s python i latex f938c24 - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Выгрузка подготовленного LaTeX-шаблона

slozhnostrukturnye analiticheskie otchety s python i latex 96dc9ef - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Результат работы pdflatex base_template_for_latex.tex. Фрагмент собранного аналитического отчета

Чтобы развернуть приложение на свободной облачной платформе Streamlit, достаточно кликнуть на Deploy this app в правой верхней части панели запущенного приложения, как изображено на рисунке. Однако предварительно необходимо зарегистрироваться на Streamlit и отправить заявку на допуск к ресурсам (обычно обработка заявки занимает несколько дней).

slozhnostrukturnye analiticheskie otchety s python i latex 45c70a5 - 🛠 Сложноструктурные аналитические отчеты с Python и LaTeX

Развертывание приложения на облачной платформе Streamlit

После успешного развертывания на платформе приложение будет доступно конечному пользователю по ссылке, как в данном случае.

Заключение

Из руководства вы узнали:

  • что собой представляет система компьютерной вёрстки LaTeX, где найти ее дистрибутивы и пакеты, как правильно читать документацию, как начать работать с системой;
  • как разрабатывать гибкие, масштабируемые шаблоны документов с помощью языка разметки TeX/LaTeX;
  • как использовать приемы разработки приложений с помощью Python-библиотеки Streamlit;
  • как может выглядеть пример связки «LaTeX + Streamlit»;
  • и, наконец, как развернуть приложение на облачной платформе Streamlit.

Полезные источники:

  • Github-репозиторий с кодом рассмотренного примера
  • Облачная реализация рассмотренного приложения с помощью платформы Streamlit

Связные материалы с платформы Proglib:

  • Туториал: визуализация данных в вебе с помощью Python и Dash
  • Преобразования Фурье для обработки сигналов с помощью Python
  • Конфигурационные файлы как инструмент управления приложениями на Python

  • 0 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Свежие комментарии

    Рубрики

    About Author 01.

    blank
    Roman Spiridonov

    Моя специальность - Back-end Developer, Software Engineer Python. Мне 39 лет, я работаю в области информационных технологий более 5 лет. Опыт программирования на Python более 3 лет. На Django более 2 лет.

    Categories 05.

    © Speccy 2020 / All rights reserved

    Связаться со мной
    Close