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
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/tyleruploads/devtkit/blob/main/LICENSE)
19
+ [![GitHub issues](https://img.shields.io/github/issues/tyleruploads/devtkit)](https://github.com/tyleruploads/devtkit/issues)
20
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devtkit = devtkit.main:main
@@ -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.