Initial commit
This commit is contained in:
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
STREAMER_NAME="example"
|
||||||
|
DISCORD_WEBHOOK_URL="https://discord.com/api/webhooks/..."
|
||||||
|
TWITCH_CLIENT_ID=""
|
||||||
|
TWITCH_CLIENT_SECRET=""
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
8
.idea/MelouGoesLive.iml
generated
Normal file
8
.idea/MelouGoesLive.iml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
4
.idea/misc.xml
generated
Normal file
4
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="Poetry (MelouGoesLive)" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/MelouGoesLive.iml" filepath="$PROJECT_DIR$/.idea/MelouGoesLive.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM python:3.11.2
|
||||||
|
|
||||||
|
COPY ./ /
|
||||||
|
WORKDIR /
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get -y dist-upgrade
|
||||||
|
RUN apt-get -y install python3-pip curl
|
||||||
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
ENV PATH=/root/.local/bin:$PATH
|
||||||
|
RUN poetry install
|
||||||
|
|
||||||
|
CMD ["poetry", "run", "python", "app/main.py"]
|
||||||
58
README.md
Normal file
58
README.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Discord Twitch Live Notification
|
||||||
|
|
||||||
|
This is a python project to send a Discord webhook with a self-updating webhook
|
||||||
|
when a specified streamer goes live on Twitch.
|
||||||
|
Checks and updates exactly once every half minute.
|
||||||
|
|
||||||
|
The motivation behind this project is that requiring discord.js or the twitch api library is too much in my opinion.
|
||||||
|
Doing this requires 7 API calls, including the really basic authentication in twitch's case.
|
||||||
|
|
||||||
|
All options to run this require environment variables. You can see them in [this file](.env).
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
The first option to run the project is to run it locally.
|
||||||
|
You may install the dependencies through pip, however it is recommended to install them with the project default, [poetry](https://python-poetry.org).
|
||||||
|
|
||||||
|
### Install prerequisites
|
||||||
|
|
||||||
|
- Python 3.11.2
|
||||||
|
|
||||||
|
Clone the repository:
|
||||||
|
```commandline
|
||||||
|
git clone https://github.com/Gadsee/Discord-Twitch-Live-Notifications.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install poetry. (Taken from the [official documentation](https://python-poetry.org/docs/).)
|
||||||
|
|
||||||
|
Debian-based linux distributions:
|
||||||
|
```bash
|
||||||
|
sudo apt-get install python3-pip curl
|
||||||
|
curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
```powershell
|
||||||
|
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py -
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```commandline
|
||||||
|
poetry env use 3.11.2
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the app
|
||||||
|
|
||||||
|
Replace `source .env` with your OS' appropriate way of loading environment variables.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry shell
|
||||||
|
source .env
|
||||||
|
python app/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running in Docker
|
||||||
|
|
||||||
|
The second option is to run the project's docker image.
|
||||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
BIN
app/__pycache__/discord_client.cpython-311.pyc
Normal file
BIN
app/__pycache__/discord_client.cpython-311.pyc
Normal file
Binary file not shown.
BIN
app/__pycache__/twitch_client.cpython-311.pyc
Normal file
BIN
app/__pycache__/twitch_client.cpython-311.pyc
Normal file
Binary file not shown.
123
app/discord_client.py
Normal file
123
app/discord_client.py
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from twitch_client import StreamInformation
|
||||||
|
|
||||||
|
|
||||||
|
class DiscordClient:
|
||||||
|
_notification_msg_id: str = ""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._webhook_url = os.environ["DISCORD_WEBHOOK_URL"]
|
||||||
|
|
||||||
|
def send_information_to_discord(
|
||||||
|
self, stream: StreamInformation, profile_image: str
|
||||||
|
) -> str:
|
||||||
|
logger.info("Sending a message with an embed to the webhook...")
|
||||||
|
streamer_url = f"https://www.twitch.tv/{stream.user_login}/"
|
||||||
|
response = requests.post(
|
||||||
|
url=f"{self._webhook_url}?wait=true",
|
||||||
|
json={
|
||||||
|
"username": "Oak Tree",
|
||||||
|
"avatar_url": "https://i.imgur.com/DBOuwjx.png",
|
||||||
|
"content": "@everyone",
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"title": stream.title,
|
||||||
|
"color": 8388863,
|
||||||
|
"timestamp": stream.started_at,
|
||||||
|
"url": streamer_url,
|
||||||
|
"author": {
|
||||||
|
"name": stream.user_name,
|
||||||
|
"url": streamer_url,
|
||||||
|
"icon_url": profile_image,
|
||||||
|
},
|
||||||
|
"image": {"url": stream.thumbnail_url},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Game",
|
||||||
|
"value": stream.game_name,
|
||||||
|
"inline": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Viewers",
|
||||||
|
"value": stream.viewer_count,
|
||||||
|
"inline": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
self._notification_msg_id = response.json()["id"]
|
||||||
|
logger.info("Stream information sent with ping to Discord.")
|
||||||
|
return self._notification_msg_id
|
||||||
|
|
||||||
|
def update_information_on_discord(
|
||||||
|
self, stream: StreamInformation, profile_image: str
|
||||||
|
) -> None:
|
||||||
|
logger.info("Updating stream information on Discord...")
|
||||||
|
streamer_url = f"https://www.twitch.tv/{stream.user_login}/"
|
||||||
|
response = requests.patch(
|
||||||
|
url=f"{self._webhook_url}/messages/{self._notification_msg_id}",
|
||||||
|
json={
|
||||||
|
"embeds": [
|
||||||
|
{
|
||||||
|
"title": stream.title,
|
||||||
|
"color": 8388863,
|
||||||
|
"timestamp": stream.started_at,
|
||||||
|
"url": streamer_url,
|
||||||
|
"author": {
|
||||||
|
"name": f"{stream.user_name}",
|
||||||
|
"url": streamer_url,
|
||||||
|
"icon_url": profile_image,
|
||||||
|
},
|
||||||
|
"image": {"url": stream.thumbnail_url},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Game",
|
||||||
|
"value": stream.game_name,
|
||||||
|
"inline": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Viewers",
|
||||||
|
"value": stream.viewer_count,
|
||||||
|
"inline": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logger.info("Message embed content updated.")
|
||||||
|
|
||||||
|
def finalize_information_on_discord(
|
||||||
|
self, streamer_name, vod_url: str | None
|
||||||
|
) -> None:
|
||||||
|
logger.info("Finalizing stream information on Discord...")
|
||||||
|
if not self._notification_msg_id:
|
||||||
|
logger.info("Message ID not set, nothing to finalize.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not vod_url:
|
||||||
|
vod_url = "None available. Please contact the developer."
|
||||||
|
|
||||||
|
response = requests.patch(
|
||||||
|
url=f"{self._webhook_url}/messages/{self._notification_msg_id}",
|
||||||
|
json={
|
||||||
|
"username": "Oak Tree",
|
||||||
|
"avatar_url": "https://i.imgur.com/DBOuwjx.png",
|
||||||
|
"content": (
|
||||||
|
f"{streamer_name} stopped the stream. VOD: \n{vod_url}"
|
||||||
|
),
|
||||||
|
"embeds": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
logger.info("Message updated with VOD.")
|
||||||
79
app/main.py
Normal file
79
app/main.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
from discord_client import DiscordClient
|
||||||
|
from twitch_client import TwitchClient, StreamInformation
|
||||||
|
|
||||||
|
|
||||||
|
class Main:
|
||||||
|
is_live: bool = False
|
||||||
|
_previous_stream: StreamInformation = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.twitch_client = TwitchClient(streamer=os.environ["STREAMER_NAME"])
|
||||||
|
self.twitch_client.update_access_token()
|
||||||
|
self.profile_image = self.twitch_client.get_streamer_profile_picture()
|
||||||
|
self.discord_client = DiscordClient()
|
||||||
|
|
||||||
|
def update_status(self):
|
||||||
|
try:
|
||||||
|
stream = self.twitch_client.get_stream()
|
||||||
|
|
||||||
|
if not stream:
|
||||||
|
if self.is_live:
|
||||||
|
logger.info("Streamer went offline.")
|
||||||
|
self.is_live = False
|
||||||
|
self.discord_client.finalize_information_on_discord(
|
||||||
|
streamer_name=self._previous_stream.user_name,
|
||||||
|
vod_url=self.twitch_client.get_vod(
|
||||||
|
user_id=self._previous_stream.user_id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.is_live:
|
||||||
|
logger.info("Streamer went live.")
|
||||||
|
self.discord_client.send_information_to_discord(
|
||||||
|
stream=stream, profile_image=self.profile_image
|
||||||
|
)
|
||||||
|
self.is_live = True
|
||||||
|
else:
|
||||||
|
logger.info("Streamer still live.")
|
||||||
|
self.discord_client.update_information_on_discord(
|
||||||
|
stream=stream, profile_image=self.profile_image
|
||||||
|
)
|
||||||
|
|
||||||
|
self._previous_stream = stream
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.exception(e)
|
||||||
|
|
||||||
|
def interrupt(self):
|
||||||
|
if not self.is_live:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.is_live = False
|
||||||
|
self.discord_client.finalize_information_on_discord(
|
||||||
|
streamer_name=self._previous_stream.user_login,
|
||||||
|
vod_url=self.twitch_client.get_vod(
|
||||||
|
user_id=self._previous_stream.user_id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logger.info("Initiating main...")
|
||||||
|
start_time = time.time()
|
||||||
|
main = Main()
|
||||||
|
|
||||||
|
logger.info("Set-up looks correct, starting main loop.")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
main.update_status()
|
||||||
|
time.sleep(30.0 - ((time.time() - start_time) % 30.0))
|
||||||
|
except (SystemExit, KeyboardInterrupt):
|
||||||
|
logger.info("Caught wish to exit, interrupting and re-raising.")
|
||||||
|
main.interrupt()
|
||||||
|
raise
|
||||||
180
app/twitch_client.py
Normal file
180
app/twitch_client.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import os
|
||||||
|
import random
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from loguru import logger
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CachePrevent:
|
||||||
|
calls: int = 0
|
||||||
|
random_number: int = 0
|
||||||
|
five_minute_update_modulo: int = 10
|
||||||
|
|
||||||
|
def prevent_cache_on_url(self, url: str) -> str:
|
||||||
|
self.calls += 1
|
||||||
|
if self.calls % self.five_minute_update_modulo == 0:
|
||||||
|
self.random_number = random.randint(0, 999999)
|
||||||
|
|
||||||
|
return f"{url}?{self.random_number}"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class StreamInformation:
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
user_login: str
|
||||||
|
title: str
|
||||||
|
game_name: str
|
||||||
|
viewer_count: int
|
||||||
|
started_at: str
|
||||||
|
_thumbnail_url: str
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail_url(self):
|
||||||
|
return (
|
||||||
|
self._thumbnail_url
|
||||||
|
.replace("{width}", "1280")
|
||||||
|
.replace("{height}", "720")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TwitchClient:
|
||||||
|
_access_token: str = ""
|
||||||
|
|
||||||
|
def __init__(self, streamer: str):
|
||||||
|
self.streamer = streamer
|
||||||
|
self._client_id = os.environ["TWITCH_CLIENT_ID"]
|
||||||
|
self._client_secret = os.environ["TWITCH_CLIENT_SECRET"]
|
||||||
|
self._cache_prevent = CachePrevent()
|
||||||
|
|
||||||
|
def update_access_token(self) -> None:
|
||||||
|
logger.info("Updating twitch access token...")
|
||||||
|
response = requests.post(
|
||||||
|
url="https://id.twitch.tv/oauth2/token",
|
||||||
|
headers={"Content-Type": "application/x-www-form-url-encoded"},
|
||||||
|
params={
|
||||||
|
"client_id": self._client_id,
|
||||||
|
"client_secret": self._client_secret,
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
self._access_token = response.json()["access_token"]
|
||||||
|
logger.info("Updating twitch access token done.")
|
||||||
|
|
||||||
|
def _update_access_token_wrapper(self) -> bool:
|
||||||
|
try:
|
||||||
|
self.update_access_token()
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.error("API call to update twitch access token failed.")
|
||||||
|
logger.exception(e)
|
||||||
|
return False
|
||||||
|
except KeyError:
|
||||||
|
logger.error("Access token was not in auth response.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_streamer_profile_picture(
|
||||||
|
self, is_retry: bool = False
|
||||||
|
) -> str | None:
|
||||||
|
response = requests.get(
|
||||||
|
url="https://api.twitch.tv/helix/users",
|
||||||
|
headers={
|
||||||
|
"Client-Id": self._client_id,
|
||||||
|
"Authorization": f"Bearer {self._access_token}",
|
||||||
|
},
|
||||||
|
params={"login": self.streamer},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
logger.info("Getting streamer returned an auth issue.")
|
||||||
|
|
||||||
|
if is_retry:
|
||||||
|
logger.error("Auth failed twice, aborting.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self._update_access_token_wrapper():
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.get_streamer_profile_picture(is_retry=True)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()["data"][0]["profile_image_url"]
|
||||||
|
|
||||||
|
def get_stream(self, is_retry: bool = False) -> StreamInformation | None:
|
||||||
|
response = requests.get(
|
||||||
|
url="https://api.twitch.tv/helix/streams",
|
||||||
|
headers={
|
||||||
|
"Client-Id": self._client_id,
|
||||||
|
"Authorization": f"Bearer {self._access_token}",
|
||||||
|
},
|
||||||
|
params={"user_login": self.streamer},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
logger.info("Getting streams returned an auth issue.")
|
||||||
|
|
||||||
|
if is_retry:
|
||||||
|
logger.error("Auth failed twice, aborting.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self._update_access_token_wrapper():
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.get_stream(is_retry=True)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
streams = response.json()["data"]
|
||||||
|
if len(streams) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
stream_data = streams[0]
|
||||||
|
return StreamInformation(
|
||||||
|
user_id=stream_data.get("user_id"),
|
||||||
|
user_name=stream_data.get("user_name"),
|
||||||
|
user_login=stream_data.get("user_login"),
|
||||||
|
title=stream_data.get("title"),
|
||||||
|
game_name=stream_data.get("game_name"),
|
||||||
|
viewer_count=stream_data.get("viewer_count"),
|
||||||
|
started_at=stream_data.get("started_at"),
|
||||||
|
_thumbnail_url=self._cache_prevent.prevent_cache_on_url(
|
||||||
|
url=stream_data.get("thumbnail_url"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_vod(self, user_id: str, is_retry: bool = False) -> str | None:
|
||||||
|
response = requests.get(
|
||||||
|
url="https://api.twitch.tv/helix/videos",
|
||||||
|
headers={
|
||||||
|
"Client-Id": self._client_id,
|
||||||
|
"Authorization": f"Bearer {self._access_token}",
|
||||||
|
},
|
||||||
|
params={"user_id": user_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 401:
|
||||||
|
logger.info("Getting vod returned an auth issue.")
|
||||||
|
|
||||||
|
if is_retry:
|
||||||
|
logger.error("Auth failed twice, aborting.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not self._update_access_token_wrapper():
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.get_vod(user_id=user_id, is_retry=True)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
vods = response.json()["data"]
|
||||||
|
if len(vods) == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
vod_data = vods[0]
|
||||||
|
return vod_data.get("url")
|
||||||
195
poetry.lock
generated
Normal file
195
poetry.lock
generated
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "23.1.0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
packaging = ">=22.0"
|
||||||
|
pathspec = ">=0.9.0"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.7.4)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2022.12.7"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.0.1"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.4"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "5.12.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama (>=0.4.3)"]
|
||||||
|
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||||
|
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loguru"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "Python logging made (stupidly) simple"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""}
|
||||||
|
win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "black (>=19.10b0)", "isort (>=5.1.1)", "Sphinx (>=4.1.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "23.0"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.11.0"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx (>=6.1.3)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.28.2"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7, <4"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = ">=2,<4"
|
||||||
|
idna = ">=2.5,<4"
|
||||||
|
urllib3 = ">=1.21.1,<1.27"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
|
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.14"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||||
|
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "win32-setctime"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "A small Python utility to set file creation time on Windows"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = "^3.11"
|
||||||
|
content-hash = "2cd384690a48bd802789a049147162c975c512098cc42b7716ba54b06f448092"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
black = []
|
||||||
|
certifi = []
|
||||||
|
charset-normalizer = []
|
||||||
|
click = [
|
||||||
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
]
|
||||||
|
colorama = []
|
||||||
|
idna = []
|
||||||
|
isort = []
|
||||||
|
loguru = []
|
||||||
|
mypy-extensions = []
|
||||||
|
packaging = []
|
||||||
|
pathspec = []
|
||||||
|
platformdirs = []
|
||||||
|
requests = []
|
||||||
|
urllib3 = []
|
||||||
|
win32-setctime = []
|
||||||
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "melougoeslive"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Deko <gadse.deko@gmail.com>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.11"
|
||||||
|
requests = "^2.28.2"
|
||||||
|
loguru = "^0.6.0"
|
||||||
|
black = "^23.1.0"
|
||||||
|
isort = "^5.12.0"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 80
|
||||||
Reference in New Issue
Block a user