• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Github. На Python-е среди звёзд.

Превью.jpg

Там царь Кащей над златом чахнет;
Одну я помню: сказку эту
Поведаю теперь я свету...
...Пушкин

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

На момент написания статьи функционал Github хостинга в «персональной user ленте» не предоставляет пользователям такой щепетильной информации, как например, «некий %username снял звезду в таком-то репозитории», но добросовестно отображает статистику по тем или иным пользователям, кто накидывает звезды проекту. Эта политика и условия использования крупнейшего хостинга IT-проектов направлена на сглаживание любых даже незначительно острых углов.

Лента.png


Скрин N1. Пример, персональная Github-лента автора, в которой отображаются лишь пользователи накидывающие звезды проекту. И ни байта информации о том, кто и когда снялся, тем самым понизив рейтинг репозитория, словно всё-всё-всё без следа поглотила Чёрная дыра, не иначе.

А когда звездочки же начинают подозрительно быстренько исчезать и без того в небольшом проекте, с ними улетучивается и частичка морального духа разработчика, а к отягощающим обстоятельствам добавляется некая сложность/дороговизна операции: просмотр аккаунтов тех username’s, кто-именно расстроился в ПО и покинул гавань.

Вот несколько задокументированных причин из Интернета, почему интернациональные пользователи снимали с репозитория свои звезды и бежали прочь в закат, не забывая при этом выразить и негатив:
  • разработчик неудачно монетизировал Open Source, чтобы хоть как-то покрыть личные затраты;
  • ультиматум по выпрашиванию звезд с угрозой приостановить разработку;
  • ПО перепродали некой конторе с ужасной репутацией;
  • агрессивный маркетинг, который дошел до новостных IT-ресурсов, где для местных экспертов упоминание лишь одного «Python» триггернуло на формирование негативного отношения к разработке у местной аудитории;
  • ненависть в явной или завуалированной пропаганде и политике размазанная по ридми;
  • имеет место и токсичность, например, разработчик отклоняет сторонние PR-ы, считая их неудачными, но неудачником могут посчитать сопровождающего код;
  • или софт без достойного обслуживания начинает банально покрываться ржавчиной и конец его жизненного цикла — предсказуем;
  • а еще, конечно же, вирутально-социальная практика, как закупка звезд у ботовладельцев.

боты.png


Скрин N2. Звёздное предложение, от которого не каждый сможет отказаться.

И так. Кроме звёздной ревизии собственного репозитория, автора иногда интересуют и сторонние разработки: например, выложенные на Github от новичков IT-проекты, которые внезапно и стремительно идут к звездному успеху, но в скором времени затухают с такой же катастрофической скоростью. Изучать причинно-следственную связь восхода и заката от проекта к проекту можно на кончиках пальцев через Github-API в среде Termux, но это еще не очень-то и удобно. Автор давно подумывал перевести свой «private script.py» в презентабельный публичный софт для парсинга звезд с репозиториев, но не имелось стимула. На днях натолкнулся на конкурс при поддержке авторов IT-шников от Codeby, где там же в личке предложение висело на счет публикации материалов. Как говорится «тут вообще все звезды сошлись», но нужно потрудиться: переработать исходники, прогнать тесты и опубликоваться. Испытывая неплохую мотивацию от предстоящего участия в конкурсе, наконец-то реализовал на публику срипт в 240 строчек кода и скомпилировал его для разных OS, чтобы пользователи в т. ч. и новички, не имеющие самого интерпретатора Python, могли без красноглазия пользоваться софтом в пару кликов. Во всяком случае автор на это надеется.

Цель программирования
Предназначение скрипта «shotstars» — находить аккаунты, с которых когда-то ставили звезды репозиториям, но затем их снимали и предоставлять такой анализ в человекочитаемом виде (неважно, можно сканировать и свои и чужие проекты), по итогу пытаться делать то, что не делает Github по умолчанию. Второстепенная функция ПО — следить за накидываемыми звездами также на выбранном промежутке времени. Что сделать или не сделать с беглецами на основе полученных результатов решает тот, у кого крутится софт. Например, выследить по нику «непорядочных людей», мультиаккаунты и отправить их в пожизненный бан; изучить портфолио ушедших профессионалов и задуматься над своими действиями, повлекшие некоторые неприятные последствия для вашего репозитория. Или предоставить простую возможность пользователям: удовлетворять звездную зависимость и задорное любопытство, на веселе чекая любые проекты.

Алгоритм
Разберемся откуда и как «shotstars» добывает информацию.

Заметка: никакого подполья, данные публичные и парсятся во имя света в белой зоне. У Github-a имеется API и подготовленная по нему когда-то с ошибками документация. IMHO, из-за символьных ошибок в doc-примерах в Интернете отсутствуют рабочие пруфы, по которым можно тренироваться парсить Github-аккаунты в кол-ве > 100 единиц. Если гуглить проблему чуть дольше обычного, то поисковики заведут и на сам же Github. Вот один из примеров, где была открыта дискуссия по проблемной документации Github-API. Ответ от транснациональной корпорации не нашелся пользователю, но в конечном итоге, тот кто открыл проблему, тот и нашел своевременное решение, поделившись наблюдениями (TL;DR, в официальной документации был предоставлен нерабочий пример с запросами к API через утилиту «Curl»).

Другой проблемный случай, цитата/перевод с сайта «stackoverflow»:
Ничего не получить. Итак, я отправил электронное письмо в службу поддержки GitHub. Вот ответ:
Спасибо, что связались с нами. Этот конкретный метод не поддерживает нумерацию страниц, поэтому мы фактически ограничиваем количество данных участников до 100 как вы обнаружили. Я не могу обещать, сможем ли/когда мы показать больше данных по этому вопросу, поскольку для нас это своего рода дорогая конечная точка. Следите за обновлениями документации для разработчиков API.
Спасибо, Винн. Итак, API статистики GitHub Repo не поддерживает нумерацию страниц. Невозможно получить список участников с более чем 100 результатами.

Суть в том, что сегодня Github-API: https://api.github.com/repos/{repo}/stargazers?per_page=100 можно листать, не ограничиваясь лишь одной страницей в 100 единиц. Но как были приведены отзывы выше: из-за неверной, необновляемой годами официальной API-документации, или по факту — дорогостоящей операции, ранее «листание» вызывало что-то вроде похожее на «404». API рабочий, но все же с некоторыми ограничениями: [60 запросов или 6К звезд/час], осталось только им воспользоваться и выводить распарсенные данные в отчеты.

Идея скрипта «shotstars» состоит в следующем
В самом начале пользователь выбирает чей репозиторий он хочет отслеживать. Далее скрипт будет парсить звезды на предмет их убывания и прибавления за промежуток времени, делая diff между полученными списками «username’s»: от предыдущего сканирования до текущего сканирования. Промежуток времени — пользовательский. Бонусом проги является то, что Github регистрация, авторизация и токены не требуются.

API.png


Скрин N3. Пример API Github-a формата JSON в 100 единиц. Утилита парсит значение ключа «login».

Перед вышеупомянутой операцией скрипт рассчитывает полное кол-во предполагаемых страниц, которое нужно будет обойти в цикле, эта операция осуществляется через смежный url Github API. Расчет кол-ва страниц производится на основе кол-ва звезд у репозитория. Пример, у случайного IT-проекта на момент написания статьи 2724 звезды, если одна страница способна отдавать максимум 100 единиц, то это будет 28 страниц/итераций. Реализованное решение на ЯП — это деление кол-ва звезд на 100 без остатка и прибавление единицы.

Почему в «shotstars» не используются параллельные запросы? Во-первых Github-API быстрое и любое с дозволения репо обрабатывается за секунды, а во-вторых Github вводит мягкий лимит на эксплуатацию API, по исчерпанию которого запросы с пользовательского IP-адреса временно блокируются (до часа).

О лимитах и ограничениях.
Сделаем подробный запрос к Github-API, например, с помощью утилиты «Curl» к репозиторию Python:
curl -v https://api.github.com/repos/python/cpython

Curl.png


Скрин N4. Возврат и чтение заголовков от сервера. Заголовок «x-ratelimit-limit» сообщает, что общий лимит по запросам = 60. Если повторить процедуру несколько раз, то увидим, что с каждым новым запросом счетчик «x-ratelimit-remaining» убывает на единицу и когда он достигнет 0 мы получим временную блокировку по IP. А когда блокировка снимется по времени? Ищем среди заголовков и приметный «x-ratelimit-reset: 1716558308». Число это — дата в секундах с начала эпохи когда лимит будет сброшен к дефолту. Вот как это находится экспериментальным путем в GNU/Linux

Curl2.png


Скрин N5. Расчет даты и времени предположительного снятия блокировки запросов к Github API в OS GNU/Linux (~45 мин).

Команда date — это текущие дата и время, а команда --date='@1716558308' — это преобразование секунд с начала эпохи в дату. Мгновенное, общее кол-во звезд, которое можно обработать прежде чем столкнуться с лимитом ожидания в 1 час = 6000, это было исследовано тоже опытным путем. То есть скрипт рассчитан на работу с небольшими и средними репозиториями по размеру звезд. Сканировать репозиторий с > 6K звездочек не имеет никакого физического смысла, выйдет error. Вместо 60 запросов/час в «shotstars» их 30/час по причине того, как сообщалось выше: один запрос на расчет кол-ва звезд/итераций у репозитория, остальные запросы на парсинг diff-звезд.

Limit.png


Скрин N6. Останов работы ПО после многократных, повторных сканирований репозитория в течение минуты.

Каждый репозиторий и производную от него data «shotstars» раскладывает по отдельным каталогам. Логика скрипта состоит в том, чтобы сравнивать каждое последующее сканирование репозитория с предыдущим, и если между ними образуется diff по недостающим пользователям, то выводить разницу по login-ам и промежутку времени в CLI-таблицы и HTML репорт. На длинной дистанции результаты по пользователям не суммируются, т.е. каждое последующее сканирование не будет повторно отображать одних и тех же по прошлым отчетам ушедших/пришедших пользователей.

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

«shotstars» поддерживает имитацию результатов. Для имитирования процесса пользователь должен один раз просканировать новый репозиторий, добавив его в БД; рандомно удалить и добавить любые строки в файл (OS GNU/Linux и Termux) /home/{user}/ShotStars/results/{repo}/new.txt
(OS Windows)
C:\Users\{User}\AppData\Local\ShotStars\result\{repo}\new.txt; запустить повторное сканирование этого же репозитория.

TL;DR. Заявленный функционал «shotstars» v0.1
По дефолту Github не предоставляет пользователям информацию по снятым звездам в проектах, «shotstars» ухитряется разрешить эту задачу.
  • Собраны готовые сборки «shotstars» для OS GNU/Linux; OS Windows; OS Android (Termux), т.е. не требуются lib-ы и Python.
  • Парсинг user’s-звезд с проверками на ошибки и ограничения.
  • Поддержка сессии; корректный останов ПО (ctrl +c); некоторые украшательства (прогресс, CLI зебры-таблицы и CLI-баннеры в цветах Codeby, в HTML отчете отсылки на IT-конкурс);
  • Отчеты в CLI и HTML форматах в т. ч. с расчетами дат.
  • Поддержка имитации результатов, задокументированный хак ПО — или побочная функция, призванная проверить работу скрипта на мертвых/стабильных репозиториях без движения звезд.
  • Работа «shotstars» рассчитана на небольшие и средние проекты до 6000 звезд и не требует регистрации, авторизации, токена Github-аккаунта.
Недостатки идеи в «shotstars».
  • Прога не на EN-языке, а на RU, ↓низкая популярность.
  • Лимит Github API 6K звезд/час.
  • Не детектируются одни и те же пользователи, которые поставили и сняли звезды в промежуток времени между сканированиями.

Галерея скриншотов

Shot stars Linux.gif


Скринкаст N1. «shotstars» for GNU/Linux.

win.png


Скрин N7. «shotstars» for Windows.


Termux logo.png


Скрин N8. «shotstars» for Android/Termux.

html report.png


Скрин N9. «shotstars» for Android/Termux. HTML-репорт.

Шаги скрипта описаны в комментариях листинга исходного кода.

$ python -m pip install requests rich
Python:
#! /usr/bin/env python3
# Copyright (c) 2024 <snoopproject@protonmail.com>

import datetime
import os
import requests
import shutil
import signal
import sys
import time
import webbrowser

from rich.console import Console
from rich.panel import Panel
from rich.progress import BarColumn, SpinnerColumn, TimeElapsedColumn, Progress
from rich.table import Table


console = Console()
Windows = True if sys.platform == 'win32' else False
Linux = True if Windows is False else False

console.print("""[yellow]
 ____  _           _     ____  _        
/ ___|| |__   ___ | |_  / ___|| |_ __ _ _ __ ___
\___ \| '_ \ / _ \| __| \___ \| __/ _` | '__/ __|
 ___) | | | | (_) | |_   ___) | || (_| | |  \__ \\
|____/|_| |_|\___/ \__| |____/ \__\__,_|_|  |___/[/yellow]  v0.1, автор: https://github.com/snooppr
""")

url_repo = input("Укажите url репозитория (Github): ")
repo = url_repo.rsplit(sep='/', maxsplit=1)[-1]
repo_api = '/'.join(url_repo.rsplit(sep='/', maxsplit=2)[-2:])

time_start = time.perf_counter()

# Создание каталогов для репозиториев.
if Windows:
    path = os.environ['LOCALAPPDATA'] + f"\\ShotStars\\results\\{repo}"
elif Linux:
    path = os.environ['HOME'] + f"/ShotStars/results/{repo}"
os.makedirs(path, exist_ok=True)


def win_exit():
    """удержание окна консоли скомпилированной версии 'windows_shotstars.exe' для OS Windows."""
    if Windows:
        input("\n'enter' выход")
        sys.exit()


def dif_time():
    """Диапазон прошедшего времени: от последнего сканирования к текущему сканированию."""
    delta = datetime.datetime.today() - datetime.datetime.fromtimestamp(date_file_new)
    return f"{delta.days}д. {(datetime.datetime.utcfromtimestamp(0) + delta).strftime('%Hч. %Mмин.')}"


def parsing(diff=False):
    """
    Так как сетевые запросы многократно обращаются к одному хосту,
    то активирована поддержка сессии для поддержания одного,
    но постоянного TCP-соединения (увеличение производительности).
    В случае неудачного подключения или сбоя HTTPAdapter реализует
    автоматически повторные попытки подключения.
    """
    my_session = requests.Session()
    repeat = requests.adapters.HTTPAdapter(max_retries=4)
    my_session.mount('https://', repeat)
    head = {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/121.0'}
    try:
        req = my_session.get(f'https://api.github.com/repos/{repo_api}', headers=head, timeout=6)
        r = req.json()
    except Exception:
        console.print('[bold red]Ошибка сети![/bold red]')
        win_exit()
        sys.exit()

# Расчет кол-ва страниц для итераций.
# В случае сбоя проверка заголовка 'X-RateLimit-Reset' от сервера на предмет расчета времени до снятия ограничений.
    try:
        stars = int(r.get("stargazers_count"))
        pages = (stars // 100) + 1
    except Exception:
        console.print("\n[bold red]Внимание! Вероятно, превышен лимит API, блокировка будет снята предположительно:",
                      time.strftime('%Y-%m-%d_%H:%M', time.localtime(int(req.headers.get('X-RateLimit-Reset')) + 60)))
        console.print(Panel.fit("Ограничения: ~лимит 30 запросов или 6000 звезд/час", title="Github API"))
        win_exit()
        sys.exit()

# Вывод на печать звезд и описание проекта (если присутствует).
    title_repo = "Описание репозитория отсутствует" if r.get('description') is None else r.get('description')
    console.print(f"[green]\n{title_repo}\n[bold green]Github-рейтинг:[/bold green] {stars} звезд\n")

# Настройка визуализации прогресса.
    progress = Progress(TimeElapsedColumn(), SpinnerColumn(spinner_name='earth'),
                        "[progress.percentage]{task.percentage:>1.0f}%",
                        BarColumn(bar_width=None, complete_style='yellow', finished_style='cyan yellow'),
                        refresh_per_second=0.4, transient=True)

# Обход и сохранение всех user's, которые ставили звезды репозиторию.
    lst_new = []
    with progress:
        task = progress.add_task("", total=pages)
        for page in range(1, pages+1):
            progress.update(task, advance=1)
            progress.refresh()
            try:
                r = my_session.get(f'https://api.github.com/repos/{repo_api}/stargazers?per_page=100&page={page}',
                                   headers=head, timeout=6).json()
       
                for num in r:
                    lst_new.append(num.get("login"))
            except Exception:
                console.print('\n[bold red]Сбой!')
                win_exit()
                sys.exit()

    with open(f"{path}/new.txt", "w", encoding="utf-8") as f_w:
        print(*lst_new, file=f_w, sep="\n")

# Сравнение списков пользователей после временных сканирований для обнаружения в них изменений.
    if diff:
        with open(f"{path}/old.txt", "r") as f_r:
            lst_old = f_r.read().split()

        diff_lst_dn = list(set(lst_old) - set(lst_new)) # убывание звезд.
        diff_lst_up = list(set(lst_new) - set(lst_old)) # прибавление звезд.

        if bool(diff_lst_dn) is False:
            console.print("[bold black on white]убывание звезд не обнаружено")
        if bool(diff_lst_up) is False:
            console.print("[bold black on white]ПРИБАВЛЕНИЕ звезд не обнаружено")

        if not any([bool(diff_lst_dn), bool(diff_lst_up)]):
            print('\nfinish', round(time.perf_counter() - time_start, 1), 'sec.') # печать времени исполнения скрипта.
            win_exit()
            sys.exit()
        elif bool(diff_lst_dn) or bool(diff_lst_up):
            per_stars_dn = round(len(diff_lst_dn) * 100 / stars, 2) # расчет % соотношения потерь звезд к общему рейтингу.
            per_stars_up = round(len(diff_lst_up) * 100 / stars, 2) # расчет % соотношения прибавления звезд к общему рейтингу.

# Настройка таблиц для вывода на печать в CLI.
            table_dn = Table(title=f"\n[yellow]Снятые звезды (-{per_stars_dn}%)\nза последние: {dif_time()}[/yellow]",
                             title_justify="center", header_style='yellow', style="yellow")
            table_dn.row_styles = ["none", "dim"]
            table_dn.add_column("N", justify="left", style="yellow", no_wrap=True)
            table_dn.add_column("gone stars", justify="left", style="yellow", no_wrap=True)

            table_up = Table(title=f"\n[cyan]Добавленные звезды (+{per_stars_up}%)\nза последние: {dif_time()}[/cyan]",
                             title_justify="center", header_style='cyan', style="cyan")
            table_up.row_styles = ["none", "dim"]
            table_up.add_column("N", justify="left", style="cyan", no_wrap=True)
            table_up.add_column("new stars", justify="left", style="cyan", no_wrap=True)

# Сохранение/открытие HTML-отчета/печать CLI-таблиц с результатами, если такие имеются.
            with open(f"{path}/report.html", "w", encoding="utf-8") as file_html:
                file_html.write(f"<!DOCTYPE html>\n<html lang='ru'>\n\n<head>\n<title>💫({repo}) HTML-отчет</title>\n" + \
                                "<meta charset='utf-8'>\n<style>\n.textcols {white-space: nowrap}\n" + \
                                ".textcols-item {white-space: normal; display: inline-block; width: 48%; " + \
                                "vertical-align: top; background: #fff2e1}\n" + \
                                ".textcols .textcols-item:first-child {margin-right: 4%}\n" + \
                                ".pic {float: right}\n.shad{display: inline-block}\n" + \
                                ".shad:hover{text-shadow: 0px 0px 14px #6495ED; transform: scale(1.1);\n" + \
                                "transition: transform 0.15s}\n</style>\n</head>\n\n<body>\n" + \
                                f"<h2 align='center' style='text-shadow: 0px 0px 13px #84d2ca' >{url_repo}</h2>\n" + \
                                "<div class='textcols'>\n<div class='textcols-item'>\n" + \
                                f"<h4 style='color:#CC3333'>💫 Снятые звезды (-{per_stars_dn}%):</h4>\n")

                if bool(diff_lst_dn):
                    file_html.write("<ol>")
                    for N, username in enumerate(diff_lst_dn, 1):
                        table_dn.add_row(f"{N}.", f'https://github.com/{username}')
                        file_html.write(f"\n<li><span class='shad'><a target='_blank' " + \
                                        f"href='https://github.com/{username}'>{username}</a></span></li>")
                    file_html.write("\n</ol>\n")

                file_html.write(f"</div>\n\n<div class='textcols-item'>\n<h4 style='color:#32CD32'>" + \
                                f"🌟 Добавленные звезды (+{per_stars_up}%):</h4>\n")

                if bool(diff_lst_up):
                    file_html.write("<ol>")
                    for N, username in enumerate(diff_lst_up, 1):
                        table_up.add_row(f"{N}.", f'https://github.com/{username}')
                        file_html.write(f"\n<li><span class='shad'><a target='_blank' " + \
                                        f"href='https://github.com/{username}'>{username}</a></span></li>")
                    file_html.write("\n</ol>\n")

                file_html.write("</div>\n</div>\n\n<br>\n<a " + \
                                "href='https://codeby.net/threads/konkurs-avtorskix-statej-2024.83387/' target='blank'><img src=" + \
                                "https://codeby.net/attachments/1200x675_konkurs_avtorov_montazhnaja_oblast_1_kopija_5-jpg.74724/ " + \
                                "alt='Программа написана специально для конкурса Codeby (май 2024)' width='600' class='pic'></a>\n\n")
                file_html.write("<span style='color:gray; text-shadow: 0px 0px 20px #333333'>" + \
                                "<small><small>╭📅 Изменения за прошедшие " + \
                                f"({dif_time()}): <br>├──{date}<br>└──{time.strftime('%Y-%m-%d_%H:%M', time.localtime())}" + \
                                "</small></small></span>\n\n<p style='color: gray'><small><small>" + \
                                "ПО разработано на конкурс от «codeby.net»<br>©Автор <a href='https://github.com/snooppr' " + \
                                "target='blank'><img align='center' src='https://github.githubassets.com/favicons/favicon.svg' " + \
                                "alt='' height='30' width='30'/>🪙</a><a href='https://yoomoney.ru/to/4100111364257544' " + \
                                "target='blank' title='Прога оказалась полезной? Поддержи финансово разработчика.'>donate" + \
                                "</a></small></small></p>\n\n</body>\n</html>")

            if bool(diff_lst_dn):
                console.print(table_dn)
            if bool(diff_lst_up):
                console.print(table_up)

            webbrowser.open(f"file://{path}/report.html")


    print('\nfinish', round(time.perf_counter() - time_start, 1), 'sec.')

    win_exit()

# Разбор пользовательского ввода url. Обновление списков прошлых сканирований или добавление нового репо в БД.
# Настройка корректного прерывания 'ctrl + c' скрипта относительно разных OS. Вызов основной функции 'parsing()'.
if __name__ == '__main__':
    try:
        if len(url_repo) == 0:
            console.print("[bold red]'enter' -> выход")
        elif len(url_repo) < 18 or 'github.com' not in url_repo:
            console.print("[bold red]Предоставлена некорректная ссылка на репозиторий")
            shutil.rmtree(path, ignore_errors=True)
            win_exit()
        elif os.path.exists(f"{path}/new.txt"):
            date_file_new = os.path.getmtime(f"{path}/new.txt")
            date = time.strftime('%Y-%m-%d_%H:%M', time.localtime(date_file_new))
            console.print(f"\nПоследняя проверка репозитория '{repo}' выполнялась ->  {date}")
            a = shutil.copy(f"{path}/new.txt", f"{path}/old.txt")
            parsing(diff=True)
        else:
            console.print(f"\n[bold green]В БД для отслеживания добавлен новый репозиторий: '{repo}'.\n" + \
                          "При последующем/повторном сканировании репозитория 'ShotStars' будет пытаться вычислять звезды.")
            parsing()
    except KeyboardInterrupt:
        console.print(f"\n[bold red]Прерывание [italic][/bold red]")
        if Windows:
            os.kill(os.getpid(), signal.SIGBREAK)
        if Linux:
            os.kill(os.getpid(), signal.SIGKILL)

ПО и материалы созданы на конкурс при поддержке IT-авторов от портала Codeby.net.

Исходник и готовые сборки выгружены на Github.
 
Последнее редактирование:

Snoop007

Green Team
07.06.2020
74
90
BIT
76
несколько задокументированных причин из Интернета, почему интернациональные пользователи снимали с репозитория свои звезды
UPD. По своей статистике (минус два десятка звезд за месяц у репозитория) самой частой причиной ~в 90% случаях снятия звезд с проекта — является удаление Github-аккаунта пользователем/модерацией.
Screenshot_20240611-071633_Termux.png
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!