devtkit 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
devtkit/main.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# --- Standard Library (STL) Imports, Alphabetical ---
|
|
3
|
+
|
|
4
|
+
import csv # To save the followers table to CSV
|
|
5
|
+
import getpass # To get the API key securely
|
|
6
|
+
import json # To save the followers table to JSON
|
|
7
|
+
import os # Assists with saving
|
|
8
|
+
import time # To not get rate limited
|
|
9
|
+
from typing import Any # To help return hints
|
|
10
|
+
|
|
11
|
+
# --- Non STL Imports, Alphabetical ---
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.1"
|
|
15
|
+
|
|
16
|
+
# Global configuration
|
|
17
|
+
FOLLOWERS_URL = "https://dev.to/api/followers/users"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def welcome_banner() -> None:
|
|
21
|
+
banner = f"""
|
|
22
|
+
=================================================================
|
|
23
|
+
devtkit (v{__version__})
|
|
24
|
+
Repository: https://github.com/tyleruploads/devtkit
|
|
25
|
+
=================================================================
|
|
26
|
+
|
|
27
|
+
This script will fetch information about you and your followers on DEV.to
|
|
28
|
+
using your API key.
|
|
29
|
+
|
|
30
|
+
SECURITY NOTICE:
|
|
31
|
+
Your API key acts like a password.
|
|
32
|
+
If an untrusted individual has access to it,
|
|
33
|
+
they have compromised your account.
|
|
34
|
+
|
|
35
|
+
Review this file here:
|
|
36
|
+
https://raw.githubusercontent.com/tyleruploads/devtkit/refs/heads/main/src/main.py
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
**************************************************************
|
|
40
|
+
"""
|
|
41
|
+
print(banner)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_formats_and_paths() -> dict[str, str]:
|
|
45
|
+
valid_formats = {"Markdown": ".md", "CSV": ".csv", "JSON": ".json"}
|
|
46
|
+
format_names = list(valid_formats.keys())
|
|
47
|
+
num_formats = len(valid_formats)
|
|
48
|
+
|
|
49
|
+
print("--- Formats ---\n")
|
|
50
|
+
|
|
51
|
+
prompt = "\n".join(
|
|
52
|
+
f"{idx}. {name}"
|
|
53
|
+
for idx, name in enumerate(valid_formats)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
while True:
|
|
57
|
+
# while True loop to handle if the user makes an invalid selection
|
|
58
|
+
|
|
59
|
+
print(prompt, "\n")
|
|
60
|
+
choices_str = input(
|
|
61
|
+
"Please enter the numbers for the "
|
|
62
|
+
"following formats you would like to save to: ",
|
|
63
|
+
).strip()
|
|
64
|
+
|
|
65
|
+
choices_num_list = list(choices_str)
|
|
66
|
+
|
|
67
|
+
# Check if there are any invalid choices
|
|
68
|
+
if any(
|
|
69
|
+
not str(x).isdigit()
|
|
70
|
+
or int(x) >= num_formats
|
|
71
|
+
for x in choices_num_list
|
|
72
|
+
):
|
|
73
|
+
print("\nYou have made an invalid selection. Please try again.\n")
|
|
74
|
+
continue
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
choices_dict = {
|
|
78
|
+
format_names[int(choice)].lower(): valid_formats[format_names[int(choice)]]
|
|
79
|
+
for choice in choices_num_list
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return ask_for_paths(choices_dict)
|
|
83
|
+
|
|
84
|
+
def ask_for_paths(choices_dict: dict[str, Any]) -> dict[str, str]:
|
|
85
|
+
formats_and_paths = {}
|
|
86
|
+
|
|
87
|
+
print("--- File Save Locations ---")
|
|
88
|
+
for format_name, format_ext in choices_dict.items():
|
|
89
|
+
while True:
|
|
90
|
+
default_name = f"followers{format_ext}"
|
|
91
|
+
path = input(
|
|
92
|
+
f"Please enter save path for {format_name.title()}"
|
|
93
|
+
f" (Default: {default_name}): ",
|
|
94
|
+
).strip()
|
|
95
|
+
|
|
96
|
+
# Use default if user did not enter path
|
|
97
|
+
if not path:
|
|
98
|
+
path = default_name
|
|
99
|
+
|
|
100
|
+
# Expand path to handle ~ symbols
|
|
101
|
+
path = os.path.expanduser(path)
|
|
102
|
+
|
|
103
|
+
# Check for directories
|
|
104
|
+
dirname = os.path.dirname(path)
|
|
105
|
+
if dirname and not os.path.exists(dirname):
|
|
106
|
+
print(
|
|
107
|
+
f"Directory '{dirname}' does not exist. Please try again.",
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
formats_and_paths[format_name] = path
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
return formats_and_paths
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def ask_for_variables() -> dict[str, Any]:
|
|
118
|
+
formats_and_paths = get_formats_and_paths()
|
|
119
|
+
|
|
120
|
+
print("\nTo get an API Key, go to: DEV.to -> Settings -> Extensions")
|
|
121
|
+
api_key = getpass.getpass("DEV.to API Key: ").strip()
|
|
122
|
+
|
|
123
|
+
val = input("Followers to pull in each GET request (default is 1000): ")
|
|
124
|
+
per_page = int(val) if val.isdigit() else 1000
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"api_key": api_key,
|
|
129
|
+
"per_page": per_page,
|
|
130
|
+
"formats_and_paths": formats_and_paths,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
def get_followers(api_key: str, per_page: int) -> dict[str, Any]:
|
|
134
|
+
headers = {
|
|
135
|
+
"api-key": api_key,
|
|
136
|
+
"User-Agent": "Mozilla/5.0",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
params = {
|
|
140
|
+
"per_page": per_page,
|
|
141
|
+
"page": 1,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
followers_dicts = []
|
|
145
|
+
|
|
146
|
+
print(f"\nA maximum of {per_page} users will be pulled from each page.")
|
|
147
|
+
|
|
148
|
+
loop_count = 0
|
|
149
|
+
while True:
|
|
150
|
+
# The loop to go through many pages if necessary
|
|
151
|
+
loop_count += 1
|
|
152
|
+
|
|
153
|
+
params["page"] = loop_count
|
|
154
|
+
|
|
155
|
+
print(f"\nPage count: {loop_count}. ")
|
|
156
|
+
|
|
157
|
+
# The while true loop that will keep going until the response is 200
|
|
158
|
+
while True:
|
|
159
|
+
response = requests.get(FOLLOWERS_URL, headers=headers, params=params)
|
|
160
|
+
|
|
161
|
+
if response.status_code == 429:
|
|
162
|
+
# HTTP 409 Too Many Requests
|
|
163
|
+
wait_time = float(response.headers.get("Retry-After", 1)) + 0.5
|
|
164
|
+
print(f"HTTP 429 Too Many Requests. Sleeping for {wait_time}s")
|
|
165
|
+
time.sleep(wait_time)
|
|
166
|
+
continue
|
|
167
|
+
if response.status_code == 200:
|
|
168
|
+
# Successfull response
|
|
169
|
+
# Sleep for 1 second to ensure the server is happy
|
|
170
|
+
# (its favorite Retry-After time is 1 second!)
|
|
171
|
+
time.sleep(1)
|
|
172
|
+
break
|
|
173
|
+
raise Exception("Error: ", response.text)
|
|
174
|
+
|
|
175
|
+
# Success, check if it contains users or if there are none
|
|
176
|
+
# If no followers were recieved,
|
|
177
|
+
# that means the last page was highest
|
|
178
|
+
|
|
179
|
+
page_followers_dicts = response.json()
|
|
180
|
+
|
|
181
|
+
if len(page_followers_dicts) >= 1:
|
|
182
|
+
followers_dicts += page_followers_dicts
|
|
183
|
+
print(
|
|
184
|
+
f"{len(page_followers_dicts)} followers pulled on page {loop_count}. "
|
|
185
|
+
f"{len(followers_dicts)} total followers have been found so far. ",
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
print(
|
|
189
|
+
f"0 followers pulled on page {loop_count}. "
|
|
190
|
+
f"{len(followers_dicts)} total followers found. \n",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# Check if the user has no followers,
|
|
194
|
+
# which would be true if this is the first page.
|
|
195
|
+
if (loop_count == 1):
|
|
196
|
+
# Tell the user there are no followers
|
|
197
|
+
# and how that will be reflected in the output
|
|
198
|
+
print(
|
|
199
|
+
"There appears to not be any followers on your account."
|
|
200
|
+
"This will be reflected in your chosen output",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return followers_dicts
|
|
204
|
+
|
|
205
|
+
def get_profile_info(profile_id: str | int, api_key: str) -> dict[str, Any]:
|
|
206
|
+
profile_info_url = f"https://dev.to/api/users/{profile_id}"
|
|
207
|
+
|
|
208
|
+
headers = {
|
|
209
|
+
"api-key": api_key,
|
|
210
|
+
"User-Agent": "Mozilla/5.0",
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
response = requests.get(profile_info_url, headers=headers)
|
|
214
|
+
|
|
215
|
+
if response.status_code == 200:
|
|
216
|
+
# Success
|
|
217
|
+
return response.json() # The profile information
|
|
218
|
+
# Error
|
|
219
|
+
raise Exception("Error: ", response.text)
|
|
220
|
+
|
|
221
|
+
def make_header() -> str:
|
|
222
|
+
# Makes the header for the top of the markdown file
|
|
223
|
+
# This does not require any variables passed
|
|
224
|
+
return """
|
|
225
|
+
# devtkit \n
|
|
226
|
+
> Generated by [devtkit](https://github.com/tyleruploads/devtkit).
|
|
227
|
+
> It is not limited to Markdown files; it supports Markdown, JSON, and CSV exports
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
def make_self_profile_header(user_info: dict[str, Any]) -> str:
|
|
231
|
+
return (
|
|
232
|
+
f"### Profile: {user_info['name']}\n\n"
|
|
233
|
+
f"| Attribute | Details |\n"
|
|
234
|
+
f"| :--- | :--- |\n"
|
|
235
|
+
f"| **Name** | {user_info['name']} |\n"
|
|
236
|
+
|
|
237
|
+
f"| **Username** | [{user_info['username']}]"
|
|
238
|
+
f"(https://dev.to/{user_info['username']}) |\n"
|
|
239
|
+
|
|
240
|
+
f"| Summary | {user_info['summary']} |\n"
|
|
241
|
+
f"| Location | {user_info['location']} |\n"
|
|
242
|
+
f"| Joined At | {user_info['joined_at']} |\n"
|
|
243
|
+
|
|
244
|
+
f"| User ID | [{user_info['id']}]"
|
|
245
|
+
f"(https://dev.to/api/users/{user_info['id']}) |\n"
|
|
246
|
+
|
|
247
|
+
f"| **Profile Picture** | <img src='{user_info['profile_image']}' width='100' alt='Profile'> |\n"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def make_profiles(followers_list: list[dict]) -> str:
|
|
251
|
+
# Check if the user has no followers
|
|
252
|
+
if len(followers_list) == 0:
|
|
253
|
+
# Return a notice that there are no followers
|
|
254
|
+
return (
|
|
255
|
+
"> NOTICE: The user that the provided API Key"
|
|
256
|
+
"belongs to does not appear to have any followers"
|
|
257
|
+
"There are no followers to make a table with"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
# End of if statement
|
|
261
|
+
|
|
262
|
+
users_md_part = """
|
|
263
|
+
| Index | Username | Name | Followed At | User ID |
|
|
264
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
for idx, follower in enumerate(followers_list):
|
|
268
|
+
# Define variables
|
|
269
|
+
name = follower["name"]
|
|
270
|
+
username = follower["username"]
|
|
271
|
+
user_id = follower["user_id"]
|
|
272
|
+
|
|
273
|
+
# created_at is follow time, not account creation time
|
|
274
|
+
followed_at = follower["created_at"]
|
|
275
|
+
|
|
276
|
+
user_md_part = (
|
|
277
|
+
f"| {idx} | [@{username}](https://dev.to/{username}) | {name} |"
|
|
278
|
+
f"{followed_at} | [{user_id}](https://dev.to/api/users/{user_id}) | \n"
|
|
279
|
+
)
|
|
280
|
+
users_md_part += user_md_part
|
|
281
|
+
|
|
282
|
+
return users_md_part
|
|
283
|
+
|
|
284
|
+
def make_markdown(followers_list: list[dict], self_info: dict[str, Any]) -> str:
|
|
285
|
+
md_string = ""
|
|
286
|
+
md_string += make_header()
|
|
287
|
+
md_string += make_self_profile_header(self_info)
|
|
288
|
+
md_string += make_profiles(followers_list)
|
|
289
|
+
return md_string
|
|
290
|
+
|
|
291
|
+
def save_files(followers_list: list[dict], formats_and_paths: dict[str, str], self_info: dict[str, Any]=None) -> None:
|
|
292
|
+
for mode, path in formats_and_paths.items():
|
|
293
|
+
if mode == "markdown":
|
|
294
|
+
with open(path, "w") as f:
|
|
295
|
+
f.write(make_markdown(followers_list, self_info))
|
|
296
|
+
|
|
297
|
+
print(f"Saved in the Markdown file format to {path}")
|
|
298
|
+
elif mode == "json":
|
|
299
|
+
with open(path, "w") as f:
|
|
300
|
+
json.dump(followers_list, f, indent=4)
|
|
301
|
+
|
|
302
|
+
print(f"Saved in the JSON file format to {path}")
|
|
303
|
+
elif mode == "csv":
|
|
304
|
+
headers = followers_list[0].keys()
|
|
305
|
+
|
|
306
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
307
|
+
writer = csv.DictWriter(f, fieldnames=headers)
|
|
308
|
+
|
|
309
|
+
writer.writeheader()
|
|
310
|
+
writer.writerows(followers_list)
|
|
311
|
+
|
|
312
|
+
print(f"Saved in the CSV file format to {path}")
|
|
313
|
+
else:
|
|
314
|
+
raise ValueError(
|
|
315
|
+
f"The mode value of {mode} is not supported"
|
|
316
|
+
"in the save_followers_table function.",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def main() -> None:
|
|
321
|
+
welcome_banner()
|
|
322
|
+
|
|
323
|
+
variables = ask_for_variables()
|
|
324
|
+
|
|
325
|
+
api_key = variables["api_key"]
|
|
326
|
+
per_page = variables["per_page"]
|
|
327
|
+
formats_and_paths = variables["formats_and_paths"]
|
|
328
|
+
|
|
329
|
+
self_info = get_profile_info("me", api_key)
|
|
330
|
+
followers_list = get_followers(api_key, per_page)
|
|
331
|
+
|
|
332
|
+
# Exit if there are no followers
|
|
333
|
+
if not followers_list:
|
|
334
|
+
print("\n[!] Exiting: No followers to export.")
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
save_files(followers_list, formats_and_paths, self_info)
|
|
338
|
+
|
|
339
|
+
if __name__ == "__main__":
|
|
340
|
+
main()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devtkit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: An open-source DEV Community toolkit written in Python that lets users export information gathered from the DEV.to API
|
|
5
|
+
Author: tyleruploads
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: api-integration,automation,cli,csv,data-export,dev-to,developer-tools,devto,forem,json,markdown,open-source,pagination,python,python-script,rate-limiting,rest-api,scripting,security,utility
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# devtkit
|
|
17
|
+
|
|
18
|
+
[](https://github.com/tyleruploads/devtkit/blob/main/LICENSE)
|
|
19
|
+
[](https://github.com/tyleruploads/devtkit/issues)
|
|
20
|
+
[](https://www.python.org/)
|
|
21
|
+
|
|
22
|
+
[DEV Tool Kit (devtkit)](https://github.com/tyleruploads/devtkit) is an open-source DEV Community toolkit written in Python that lets users export information gathered from the DEV.to API.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
Currently, there are 2 ways to install devtkit
|
|
26
|
+
|
|
27
|
+
<details>
|
|
28
|
+
<summary>Universal (Windows, Linux, and macOS)</summary>
|
|
29
|
+
<br>
|
|
30
|
+
|
|
31
|
+
### Install pipx (if you do not have it)
|
|
32
|
+
* **Windows**:
|
|
33
|
+
```cmd
|
|
34
|
+
pip install pipx
|
|
35
|
+
pipx ensurepath
|
|
36
|
+
```
|
|
37
|
+
* **Linux (Debian/Ubuntu):**
|
|
38
|
+
```bash
|
|
39
|
+
sudo apt update
|
|
40
|
+
sudo apt install pipx
|
|
41
|
+
pipx ensurepath
|
|
42
|
+
```
|
|
43
|
+
* **Linux (Fedora):**
|
|
44
|
+
```bash
|
|
45
|
+
sudo dnf install pipx
|
|
46
|
+
pipx ensurepath
|
|
47
|
+
```
|
|
48
|
+
* **macOS (via [Homebrew](https://brew.sh/)):**
|
|
49
|
+
```bash
|
|
50
|
+
brew install pipx
|
|
51
|
+
pipx ensurepath
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Installing the CLI Utility
|
|
55
|
+
Now that you have installed `pipx`, run the following command to install [`devtkit`](https://github.com/tyleruploads/devtkit):
|
|
56
|
+
```bash
|
|
57
|
+
pipx install devtkit
|
|
58
|
+
```
|
|
59
|
+
</details>
|
|
60
|
+
|
|
61
|
+
<details>
|
|
62
|
+
<summary>Clone the repository and run the script</summary>
|
|
63
|
+
<br>
|
|
64
|
+
|
|
65
|
+
If you want to run the script, view the source code, or contribute, clone the repository locally:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/tyleruploads/devtkit.git
|
|
69
|
+
cd devtkit
|
|
70
|
+
|
|
71
|
+
pip install -r requirements.txt
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
> If you are getting a PEP 668 error, ensure you have [`pipx`](https://pipx.pypa.io/stable/) installed and run:
|
|
75
|
+
> `pipx install --editable .`
|
|
76
|
+
|
|
77
|
+
**If you installed with pipx**:
|
|
78
|
+
Run `devtkit`
|
|
79
|
+
|
|
80
|
+
**Otherwise:**
|
|
81
|
+
Run `python3 src/main.py`
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
</details>
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
To use devtkit, simply follow along with the prompts the script gives you.
|
|
89
|
+
|
|
90
|
+
> To get a DEV.to API Key, navigate to: DEV.to -> Settings -> Extensions, and scroll till you find "DEV Community API Keys"
|
|
91
|
+
> The API Key you generate will still be available for you to see after you close the tab, so you do not need to save it (unlike most API Keys)
|
|
92
|
+
|
|
93
|
+
An example run is:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
--- Formats ---
|
|
97
|
+
|
|
98
|
+
0. Markdown
|
|
99
|
+
1. CSV
|
|
100
|
+
2. JSON
|
|
101
|
+
|
|
102
|
+
Please enter the numbers for the following formats you would like to save to: 012
|
|
103
|
+
--- File Save Locations ---
|
|
104
|
+
Please enter save path for Markdown (Default: followers.md): ~/Documents/followers.md
|
|
105
|
+
Please enter save path for Csv (Default: followers.csv): ~/Documents/followers.csv
|
|
106
|
+
Please enter save path for Json (Default: followers.json): ~/Documents/followers.json
|
|
107
|
+
|
|
108
|
+
To get an API Key, go to: DEV.to -> Settings -> Extensions, and scroll to the bottom.
|
|
109
|
+
DEV.to API Key: (securely collected with the getpass module from the Python STL)
|
|
110
|
+
Followers to pull in each GET request (default is 1000):
|
|
111
|
+
|
|
112
|
+
A maximum of 1000 users will be pulled from each page.
|
|
113
|
+
|
|
114
|
+
Page count: 1.
|
|
115
|
+
534 followers pulled on page 1. 534 total followers have been found so far.
|
|
116
|
+
|
|
117
|
+
Page count: 2.
|
|
118
|
+
0 followers pulled on page 2. 534 total followers found.
|
|
119
|
+
|
|
120
|
+
Saved in the Markdown file format to /home/tyler/Documents/followers.md
|
|
121
|
+
Saved in the CSV file format to /home/tyler/Documents/followers.csv
|
|
122
|
+
Saved in the JSON file format to /home/tyler/Documents/followers.json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Features
|
|
126
|
+
devtkit has a large array of features that make it stand out from projects like it.
|
|
127
|
+
|
|
128
|
+
* **Multi-Format Export**: Save to Markdown, CSV, and JSON files
|
|
129
|
+
* **Secure API Key Handling**: Securely collects the user's DEV.to API Key with the Python STL Module getpass and only uses it to interact with the DEV.to API endpoint
|
|
130
|
+
* **Smart Rate-Limiting**: Automatically handles `429 Too Many Requests` responses
|
|
131
|
+
* **Beautiful and Detailed Output**: Outputs a beautiful and detailed Markdown file, a detailed CSV or JSON file, or all 3
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Contributions are what make open-source projects important. All contributions are highly appreciated
|
|
136
|
+
|
|
137
|
+
* **Found a bug or issue**: Open an Issue and show the output of the script, the steps to reproduce it, and as much information as possible
|
|
138
|
+
* **Have an idea**: Open an Issue and explain your idea as much as possible, why you think it would be a good addition to the project, and any other important information.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
[MIT](https://choosealicense.com/licenses/mit/)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
devtkit/main.py,sha256=lspYpPuKHMKt4Y0fafx98EXrdwlJ3C-vlZBBcng9ZBI,10876
|
|
2
|
+
devtkit-0.1.1.dist-info/METADATA,sha256=JGWHmWQbBAXVXalLVGWKi9HXNCcGCZBUsyvNA5-jMHM,4951
|
|
3
|
+
devtkit-0.1.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
4
|
+
devtkit-0.1.1.dist-info/entry_points.txt,sha256=Af0T1HJxU6RiWCJXdzS0B-CnAM7eZFdhT5I65zClPPw,46
|
|
5
|
+
devtkit-0.1.1.dist-info/licenses/LICENSE,sha256=iEZi_9PDjIFPycmHfkozpw8JqL2uNdqiX7jP-OlL7Ps,1095
|
|
6
|
+
devtkit-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
|
|
2
|
+
Copyright (c) 2026 Tyler N. <https://github.com/tyleruploads>
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
5
|
+
|
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|