В разрезе: новостной агрегатор на Android с бэкендом. Система контроля версий |
Метки: author fedor_malyshkin программирование github git vcs smartgit |
В разрезе: новостной агрегатор на Android с бэкендом. Вводная часть, идея, технологии |
Метки: author fedor_malyshkin разработка под android программирование java android |
Что нужно знать владельцам сайтов, чтобы не потерять свой бизнес? |
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
|
[Перевод] Аналоговый мир и его иллюзия |
Метки: author PatientZero разработка игр иллюзия выбора |
Осторожно — майнеры-халявщики |
Метки: author KeisJones хостинг облачные вычисления виртуализация mining monero iaas cloud |
[Из песочницы] Нетривиальные проблемы с generic'ами и возможные решения |
public void foobar(Map ms) {
...
}
public void foobar(Map ms) {
...
}
public void foobar(Map ms) {
...
}
List
Метки: author kobaeugenea java generic spring hibernate |
«Работаю над проектами, объединяющими книгу и интерактив»: Кей Хорстманн о книгах и не только |
public class Greeter
{
public Greeter()
{
System.out.println("Hello, World!");
}
}
if (args > 0) {
printf(args[0]);
}
if (args > 0)
{
printf("%s\n", args[0]);
}
if (args > 0)
{ printf("%s\n", args[0]);
}
Метки: author phillennium java блог компании jug.ru group core java cay horstmann joker книги конференция |
Мониторинг Raspberry PI |
Возникла передо мной такая задача: сделать мониторинг Raspberry PI. И требования:
Здесь и далее под мониторингом системы я буду понимать сбор time series данных. Например, JVM heap size или количество обработанных сообщений за интервал.
Самодостаточность автоматически означает, что данные надо хранить локально. Отображать их надо в браузере, потому что уже есть вэб-админка для этого. Итак, что мы имеем из современного:
Все остальные варианты найденные на просторах, не подошли либо потому, что надо вручную делать data retention, либо требовательны к ресурсам, либо уж совсем наколеночные.
А что если продолжить мысль про RRD и Java? Получается RRD4J. Эта библиотека на Java, которая полностью поддерживает все операции и возможности оригинального rrdtool. Единственное отличие — это несовместимость баз данных между rrdtool и RRD4J. Но с другой стороны это даже лучше. Базы, созданные оригинальным rrdtool, бинарно несовместимы между различными архитектурами.
Итак, RRD. Он идеально подходит для Raspberry PI:
Но и не без проблем:
И тут мне в голову приходит осознание. Мы же в 2017 году! Время, когда у нас есть стандарты на передачу бинарных файлов в браузер и разные мощные javascript библиотеки для рисования графиков. Что если передавать скачивать RRD базу с сервера как есть, вытаскивать из неё данные и рисовать уже какой-нибудь готовой и проверенной временем Javascript библиотекой?
Посидев несколько ночей в попытке понять как писать на Javascript и создать плагин для Jquery (а на нём ещё модно писать?), я создал rrd4j-js.
Суть проекта достаточно проста: скачивать RRD, парсить и передавать данные для отрисовки во flot. А уже плагинами flot добивать нужные стили и интерактив. В итоге, решение оказалось даже лучше, чем стандартные графики rrdgraph:
Библиотека получилась достаточно простая. Больше всего времени конечно заняло выяснение конвенций по оформлению кода, созданию классов (sic!) в javascript и попытке поделиться проектом с миром.
Я с самого начала решил сделать самодостаточную библиотеку, которую можно загрузить в npm. После нескольких попыток это сделать, у меня, конечно же, всё получилось. Но тут же выяснилось, что npm используется только для server-side разработки на nodejs. И нельзя просто так зарелизить библиотеку в правильный репозиторий. Да что тут стесняться: нельзя понять какой из репозиториев правильный. В итоге я остановился на npm. Может кто-нибудь сведущий подскажет как правильно?
С получившимся инструментом, уже можно было начинать творить. А именно периодически сохранять метрики в RRD4J. Обвязка в виде достаточно распространённых metrics работающая в compact1 — приятное дополнение. В итоге пришлось написать достаточно простой RRD4JReporter, который расширяет com.codahale.metrics.ScheduledReporter и пользоваться в удовольствие.
Метки: author dernasherbrezon javascript java rrdtool rrd raspberry pi monitoring metrics flot jquery plugins |
[Перевод] Восемнадцатилетнего парня арестовали за то, что обнаружил ошибку в будапештской системе по продаже электронных билетов |
Метки: author nanton информационная безопасность блог компании everyday tools безопасность веб-приложений законодательство и ит it- сообщество хакерство этичный хакинг |
Приглашаю на летние открытые лекции по игровой индустрии в ВШБИ |
Метки: author viacheslavnu я пиарюсь игровая индустрия геймдев gamedev геймдизайн открытые лекции менеджмент игровых проектов вшби |
Команда веб-студии: система роста, аттестация и мотивация |
|
Китай ужесточает регулирование VPN |
На данный момент нет пояснений относительно того, как будут обстоять дела в случае, если человек предпочитает работать из дома или поедет в командировку
Метки: author VASExperts блог компании vas experts vas experts vpn китайский файрвол |
grab'им караваны ЛитРес в ознакомительных целях |
Некоторые книги, по требованию правообладателя, доступны только для чтения с сайта или в приложениях ЛитРес. Все бы ничего, но бывают такие ситуации:
Право читать обошлось в 2/3 от стоимости бумажного носителя, если брать с сайта издательства.
Справедливости нет. есть только я
И тут я решил написать grabber.
За основу взял QWebEngineView, что бы не заморачиваться с авторизацией. И внешне это выглядит так:
Для этого в Qt есть QWebEngineCookieStore и
QNetworkCookieJar
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
m_ui(new Ui::MainWindow),
m_store(nullptr),
m_cookieJar(new QNetworkCookieJar (this)),
m_networmManager(new QNetworkAccessManager(this)),
m_try(0),
m_currentPage(0),
m_capches(1)
{
m_ui->setupUi(this);
m_store = m_ui->webView->page()->profile()->cookieStore();
Q_ASSERT(m_store != nullptr);
connect(m_store, &QWebEngineCookieStore::cookieAdded, this, &MainWindow::handleCookieAdded);
m_store->loadAllCookies();
m_ui->webView->load(QUrl("https://www.litres.ru/"));
m_networmManager->setCookieJar(m_cookieJar);
connect(m_networmManager, &QNetworkAccessManager::finished,
this, &MainWindow::handleImage);
}
void MainWindow::handleCookieAdded(const QNetworkCookie &cookie)
{
m_cookieJar->insertCookie(cookie);
}
Когда переходим на чтение книги и нажимаем на кнопку Grab, то берется url вида:
https://www.litres.ru/static/or3/view/or.html?art_type=4&file=26599915&bname=Разработка веб-приложений в ReactJS&cover=%2Fstatic%2Fbookimages%2F26%2F59%2F99%2F26599923.bin.dir%2F26599923.cover.jpg&art=22880082&user=Что-то&uuid=Что-то
Вытаскиваем id файла и название:
void MainWindow::onGrabButtonClicked()
{
if(!parseUrl(m_ui->webView->url()))
{
return;
}
const auto paths = QStandardPaths::standardLocations(QStandardPaths::DownloadLocation);
if (paths.isEmpty()) {
qWarning()<<"There is no standard path to download";
return;
}
downloadTo(*paths.begin());
}
bool MainWindow::parseUrl(const QUrl &url)
{
const auto query = QUrlQuery(url.query(QUrl::FullyDecoded));
if (query.isEmpty()){
return false;
}
static const QVector fields = {
"file", "bname", "uuid"
};
for (const auto& key: fields) {
if (!query.hasQueryItem(key)) {
qWarning()<<"Query hasn't param"<< key;
return false;
}
}
m_name = query.queryItemValue("bname", QUrl::FullyDecoded);
m_file = query.queryItemValue("file");
m_format = "jpg";
return true;
}
MainWindow::downloadTo настраивает QPdfWriter и QPainter
void MainWindow::downloadTo(const QString &path)
{
QDir dir(path);
m_writer = std::make_unique(dir.absoluteFilePath(m_name+".pdf"));
QPageLayout layout(QPageSize(QPageSize::A4), QPageLayout::Portrait,
QMarginsF(0,0,0,0));
m_writer->setPageLayout(layout);
m_writer->setResolution(96);
m_writer->setTitle(m_name);
m_painter = std::make_unique();
m_painter->begin(m_writer.get());
nextImage();
}
Страницы скачиваются по url вида:
https://www.litres.ru/pages/read_book_online/?file=26599915&page=2&rt=w1280&ft=gif
Параметр | Описание |
---|---|
rt | отвечает за размеры, принимает значение w640, w1280 |
ft | формат gif или jpg |
page | номер страницы |
file | идентификатор файла |
Формат jpg применяется для страниц с графикой, в то же время gif для текста.
Если страницы по url: https://www.litres.ru/pages/read_book_online/?file=26599915&page=0&rt=w1280&ft=gif
не существует, то следует запросить https://www.litres.ru/pages/read_book_online/?file=26599915&page=0&rt=w1280&ft=jpg
Получаем:
void MainWindow::nextImage()
{
QUrlQuery query;
query.addQueryItem("file", m_file);
query.addQueryItem("rt", "w640");
query.addQueryItem("ft", m_format);
query.addQueryItem("page", QString::number(m_currentPage));
QUrl url(BasePath);
url.setQuery(query);
m_networmManager->get(QNetworkRequest(url));
++m_currentPage;
}
void MainWindow::handleImage(QNetworkReply *reply)
{
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning()<<"Network error"<errorString();
if(m_try == 3) {
m_painter->end();
m_painter.reset();
m_writer.reset();
return;
}
if (m_format == "gif") {
m_format = "jpg";
} else {
m_format = "gif";
}
--m_currentPage;
++m_try;
nextImage();
return;
}
m_try = 0;
qDebug()<<"Write page"<url();
std::string f;
if (m_format == "jpg") {
f = "JPEG";
} else {
f = "GIF";
}
const auto data = reply->readAll();
const auto source = QImage::fromData(data, f.c_str());
if (source.isNull()) {
//handleCapcha(data, reply->url());
--m_currentPage;
nextImage();
return;
}
m_ui->pages->setText(QString::number(m_currentPage));
const auto dest = source.scaledToWidth(m_writer->width()/*, Qt::SmoothTransformation */);
m_painter->drawImage(QPoint(0,0), dest);
m_writer->newPage();
nextImage();
}
Капча вроде бы есть, но в тоже время нет. Выскакивает не всегда
Мы заметили странную активность с вашего компьютера. Возможно, мы ошиблись, и эта активность идёт не от вас. В таком случае, подтвердите, что вы не робот и продолжайте пользоваться нашим сайтом.
Оказалось, что можно просто перезапросить страницу и дальше продолжить скачивание изображений. Если же вам не нравится прикидываться роботом, то можно это обработать:
void MainWindow::handleCapcha(const QByteArray &page, const QUrl &url )
{
++m_capches;
m_ui->webView->page()->setHtml(page, url);
m_ui->captches->setText(QString::number(m_capches));
QEventLoop loop;
constexpr int duration = 1000*60*5;
QTimer::singleShot(duration, &loop, &QEventLoop::quit);
loop.exec();
}
Тут загружаем в WebView страницу с капчей. После чего, можем ввести капчу.
Книга объемом 256 страниц в PDF со страницами A4 и DPI 96 весит 51,7 МБ против 5,8 МБ зашифрованного документа.
Код доступен на GitHubGist
Метки: author RPG18 программирование qt c++ qt5 |
Pygest #14. Релизы, статьи, интересные проекты из мира Python [18 июля 2017 — 31 июля 2017] |
Метки: author andrewnester разработка веб-сайтов программирование машинное обучение python django digest pygest machine learning flask дайджест web deep learning |
[recovery mode] Работа с базами данных в среде разработки 3CX Call Flow Designer |
SELECT count(*) FROM customers WHERE id={0}
EQUAL(validatePIN.ScalarResult,1)
|
Play with Docker — онлайн-сервис для практического знакомства с Docker |
docker
.docker/ucp
, docker/ucp-agent
, docker/ucp-auth
, docker/ucp-swarm
…) для запуска в контейнерах:[node4] (local) root@10.0.7.6 ~
$ docker run --privileged docker/ucp
docker/ucp docker/ucp-controller:2.1.5
docker/ucp-agent docker/ucp-dsinfo
docker/ucp-agent:2.1.5 docker/ucp-dsinfo:2.1.5
docker/ucp-auth docker/ucp-etcd
docker/ucp-auth-store docker/ucp-etcd:2.1.5
docker/ucp-auth-store:2.1.5 docker/ucp-hrm
docker/ucp-auth:2.1.5 docker/ucp-hrm:2.1.5
docker/ucp-cfssl docker/ucp-metrics
docker/ucp-cfssl:2.1.5 docker/ucp-metrics:2.1.5
docker/ucp-compose docker/ucp-swarm
docker/ucp-compose:2.1.5 docker/ucp-swarm:2.1.5
docker/ucp-controller docker/ucp:2.1.5
# На первом узле кластера, который станет менеджером:
[node1] (local) root@10.0.7.3 ~
$ docker swarm init --advertise-addr 10.0.7.3
Swarm initialized: current node (txh3ffph72xarxjeg9gmpra2s) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-698vpn9u804ik4xdc9by60ytdabx3kuzyxj3vzhtr74qvkdlja-7xa6pwit58xzun989tao2nis7 10.0.7.3:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
[node1] (local) root@10.0.7.3 ~
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
txh3ffph72xarxjeg9gmpra2s * node1 Ready Active Leader
# Теперь перейдите к консоли другого узла (например, node2)
[node2] (local) root@10.0.7.4 ~
$ docker swarm join --token SWMTKN-1-698vpn9u804ik4xdc9by60ytdabx3kuzyxj3vzhtr74qvkdlja-7xa6pwit58xzun989tao2nis7 10.0.7.3:2377
This node joined a swarm as a worker.
# Вернитесь к консоли первого узла (он является менеджером кластера)
[node1] (local) root@10.0.7.3 ~
$ docker node ls
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS
szx0qqvj5zwt6a4nho9an54yx node2 Ready Active
txh3ffph72xarxjeg9gmpra2s * node1 Ready Active Leader
franela/dind
), но при желании его можно модифицировать (использовать свой базовый образ).Dockerfile
).
Метки: author shurup системное администрирование виртуализация devops *nix блог компании флант docker контейнеры alpine linux обучение |
Интерфейс vs interface |
Одним из принципов объектно-ориентированного проектирования является программирование на уровне интерфейса, а не на уровне реализации. Видимо, из-за того что код в книгах и статьях по проектированию представлен преимущественно на Java, программисты на других языках, особенно с динамической типизацией, испытывают трудности с переносом знаний из этих книг и статей на свой рабочий язык программирования.
Часто сложность в понимании принципа "программируйте на уровне интерфейса" кроется в концентрации на инструменте, а не на смысле. Из-за наличия в Java ключевого слова interface
, происходит искажение понимания принципа, и он превращается в "программируйте, используя interface
". Так как в Python инструмент в виде ключевого слова interface
отсутствует, некоторые питонисты пропускают этот принцип.
В книге Банды Четырех примеры приводятся на Smalltalk и C++. Оба этих языка не имеют ключевого слова interface
, но это не мешает авторам применять принцип, используя имеющиеся в распоряжении конструкции языка:
У манипулирования объектами строго через интерфейс абстрактного класса есть два преимущества:
- клиенту не нужно иметь информации о конкретных типах объектов, которыми он пользуется, при условии, что все они имеют ожидаемый клиентом интерфейс;
- клиенту необязательно "знать" о классах, с помощью которых реализованы объекты. Клиенту известно только об абстрактном классе (или классах), определяющих интерфейс.
Данные преимущества настолько существенно уменьшают число зависимостей между подсистемами, что можно даже сформулировать принцип объектно-ориентированного проектирования для повторного использования: программируйте в соостветствии с интерфейсом, а не с реализацией.
Но даже приведенные в цитате преимущества не являются единственными, если посмотреть на принцип под более широким углом.
Самое общее определение интерфеса из русскоязычной Википедии выглядит так:
Интерфейс — "общая граница" между отдельными системами, через которую они взаимодействуют; совокупность средств и правил, обеспечивающих взаимодействие отдельных систем.
Человек является системой, а значит каждый из нас ежедневно сталкивается со множеством интерфейсов для взаимодействия с другими системами. Примеры интересных интрефейсов в реальном мире можно посмотреть в постах от Мосигры (раз, два, три, четыре). Взаимодействие с хорошим интерфейсом происходит без лишних хлопот, мы не замечаем как используем его. Более того, взаимодействуя даже с самым ужасным интерфейсом, мы обращаем внимание только на неудобство интерфейса, в то время как реализация в обоих случаях скрыта от наших глаз.
При работе с микроволновой печью мы пользуемся совокупностью средств: элементы управления для включения печи, установки времени, мощности, режима разогрева. А также совокупностью правил: чтобы блюдо разогрелось, нужно поставить его внутрь печи, закрыть дверцу и запустить разогрев. Согласитесь, если бы для разогрева обеда нам требовалось изменять электрическую схему печи, настраивая тем самым частоту работы магнетрона, это доставляло бы неудобство. Управление печью при помощи интерфейса позволяет нам не только уменьшить время и трудозатраты для достижения конечной цели — разогреть обед, но и дает возможность использовать знания интерфеса при работе с микроволновыми печами других видов и от других производителей.
Концептуально, смысл интерфейсов в программном коде не отличается от такового в реальном мире. Интерфейс — это все таже "общая граница" между отдельными системами. Только в данном случае системы — это сервисы, микросервисы, пакеты, классы или даже функции. Каждая из этих единиц программного кода является системой со своей границей, через которую необходимо взаимодействовать и которую не нужно нарушать.
Описанная выше ситуация о необходимости изменения электрической схемы микроволновой печи для разогрева обеда кажется абсурдной. Но программисту приходится сталкиваться с подобным в ежедневной практике. Даже плохое название метода, которое не выражает его намерения, может привести к раскрытию реализации:
class UserCollection:
# Код инициализации пропущен
def linear_search_for(user: User) -> bool:
for saved_user in self._all_users:
if saved_user == user:
return True
return False
Такое название говорит нам об использованном внутри алгоритме, а также о структуре данных, которая лежит в основе UserCollection
. Вся эта информация является лишней на данном уровне абстракции, плохо передает намерения и неудобна для дальнейшего расширения. Чтобы сделать интерфейс более чистым, выразим в имени метода "что" делает код, а не "как" он это делает:
class UserCollection:
# Код инициализации пропущен
def includes(user: User) -> bool:
''' Любая необходимая реализация '''
Отражающие суть названия вносят большой вклад в возможность программировать на уровне интерфейса, но только названий недостаточно для реализации принципа. Например, эту простую функцию, имеющую понятное название, сложно использовать, зная только интерфейс:
from utils import DatabaseConfig
# DatabaseConfig предоставляет доступ к конфигу,
# который хранится в БД
def is_password_valid(password: str) -> bool:
min_length = DatabaseConfig().password_min_length
return len(password) > min_length
Интерфейс обманывает нас, заявляя, что для работы достаточно только пароля. Вызов этой функции в окружении, где необходимая база данных не поднята, приведет к ошибке, что заставит обратиться к реализации. Обогатим интерфейс, чтобы избавиться от необходимости обращаться к реализации. В данном случае подойдет явная передача параметра min_length
:
# Зависимость от DatabaseConfig больше не нужна
def is_password_valid(password: str, min_length: int) -> bool:
return len(password) > min_length
Неявная зависимость от DatabaseConfig
устранена. В дополнение к этому мы получили пригодную для тестирования функцию, которая представляет из себя настоящий черный ящик со всеми необходимыми входными параметрами и известным типом выходного параметра.
Неявные зависимости раскрывают реализацию любой единицы программного кода. Python позволяет писать код, который будет исполнен при импорте файла. На первый взгляд это может показаться безобидным, но создаваемая таким образом неявная зависимость влечет за собой много проблем:
# utils.py
class DatabaseConfig:
''' Инициализация класса создает соединение с базой данных '''
config = DatabaseConfig()
# В этом же файле лежит функция с хорошим интерфейсом
def is_password_valid(password: str, min_length: int) -> bool:
return len(password) > min_length
# user.py
from utils import is_password_valid
# В момент импорта происходит инициализация
# DatabaseConfig и подключение к БД
class User:
def __init__(self, name: str, password: str):
self.name = name
self.password = password
def change_password(self, new_password: str) -> None:
if not is_password_valid(new_password, min_length=6):
raise Exception('Invalid password')
self.password = new_password
Импорт класса User в интерпретатор или тест, если необходимая БД не запущена, снова закончится ошибкой, которая раскрывает реализацию. Изменять интерфейс класса нет смысла, так как причиной бед в данном случае является выражение from utils import is_password_valid
, а именно глобальная переменная, которая создается во время импортирования. Еще один недостаток глобальной переменной — она может стать причиной невозможности программировать на уровне интерфейса. Решить возникшую проблему можно с помощью создания экземпляра DatabaseConfig
в момент старта приложения и явной передачи экземпляра всем заинтересованным объектам.
Отсутствие неявных зависимостей и отражающие суть названия, защищая нас от деталей реализации, все еще не позволяют получить все преимущества программирования на уровне интерфейса. Самое время обратиться к программированию строго через интерфейс абстрактного класса. Кент Бек в книге "Smalltalk Best Practice Patterns" пишет:
There are a few things I look for that are good predictors of whether a project is in good shape.
…
Replacing objects — Good style leads to easily replaceable objects. In a really good system, every time the user says “I want to do this radically different thing,” the developer says, “Oh, I’ll have to make a new kind of X and plug it in.”
Использование интерфейса, определенного абстрактным классом, вместо конкретного класса — это удобный прием для создания заменяемых объектов. В Python есть возможность создавать абстрактные классы при помощи модуля стандартной библиотеки abc, но для компактности кода в примерах будет использоваться подход, когда нереализованные методы абстрактного класса выбрасывают NotImplementedError
.
Допустим, нам необходимо реализовать отображение прогноза погоды на сегодня и на текущую неделю. Прогноз погоды мы получаем с некоторого стороннего ресурса. Чтобы не привязываться к конкретному ресурсу, а также к возвращаемому ресурсом формату данных, нужно формализовать способ общения в виде абстрактного класса, а формат данных в виде объекта-значения:
# weather.py
from typing import List, NamedTuple
class Weather(NamedTuple):
max_temperature_с: int
avg_temperature_с: int
min_temperature_c: int
class WeatherService:
def get_today_weather(self, city: str) -> Weather:
raise NotImplementedError
def get_week_weather(self, city: str) -> List[Weather]:
raise NotImplementedError
Не имея конкретной реализации, клиент нашего кода, опираясь на предоставленные интерфейсы, уже сможет начать тестирование и разработку, используя вместо реального сервиса подменные объекты:
# test.py
from client import WeatherWidget
from weather import Weather, WeatherService
class FakeWeatherService(WeatherService):
def __init__(self):
self._weather = Weather(max_temperature_с = 24,
avg_temperature_с = 20,
min_temperature_c = 16)
def get_today_weather(self, city: str) -> Weather:
return self._weather
def get_week_weather(self, city: str) -> List[Weather]:
raise [self._weather for _ in range(7)]
def test_present_today_weather_in_string_format():
weather_service = FakeWeatherService()
widget = WeatherWidget(weather_service)
expected_string = ('Maximum Temperature: 24 °C'
'Average Temperature: 20 °C'
'Minimum Temperature: 16 °C')
assert widget.today_weather == expected_string
Интерфейс дает нам гибкость: если наших пользователей не устроит точность прогноза, мы сможем легко переключиться на другой ресурс прогноза погоды, написав класс, реализующий интерфейс WeatherService
.
Использование принципа "программируйте в соответствии с интерфейсом, а не с реализацией" позволяет создавать более гибкий дизайн приложения, разгрузить голову разработчика и улучшить коммуникации внутри команды. Все это делает систему более пригодной для поддержки и добавления новой функциональности. В Python нет ключевого слова interface
, но есть другие способы реализации принципа: отражающие суть названия, устранение неявных зависимостей и использование абстрактных классов. Давайте чаще обращать внимание на суть принципов, лежащих в основе хорошего кода, а не концентрировать все свое внимание на инструментах.
Метки: author Telichkin совершенный код ооп python интерфейс чистый код |
Собеседования, рынок труда и прочее в городе Москве обр. лета 7525 |
Метки: author Scif_yar читальный зал кадры кадровик hr карьера |
Magento Dare to Share — Открытая Площадка для докладов о Magento, PHP и eCommerce |
Метки: author maghamed разработка под e-commerce php magento magento 2 ecommerce solid magento2 varnish |