devtkit 0.1.1__tar.gz

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.
@@ -0,0 +1,38 @@
1
+ name: Publish to PyPi
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*' # Only runs workflow when tag starting with v is pushed
7
+
8
+ jobs:
9
+ pypi-publish:
10
+ name: Upload release to PyPI
11
+ runs-on: ubuntu-latest
12
+
13
+ environment:
14
+ name: pypi
15
+ url: https://pypi.org/project/devtkit
16
+
17
+ permissions:
18
+ id-token: write
19
+ contents: read
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+
24
+ - name: Set up Python
25
+ uses: actions/setup-python@v4
26
+ with:
27
+ python-version: "3.x"
28
+
29
+ - name: Install build dependencies
30
+ run: python -m pip install --upgrade pip build setuptools wheel
31
+
32
+ - name: Build package
33
+ run: python -m build
34
+
35
+ - name: Publish package distributions to PyPI
36
+ uses: pypa/gh-action-pypi-publish@release/v1
37
+ with:
38
+ skip-existing: true
@@ -0,0 +1,259 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python,linux,windows,macos
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python,linux,windows,macos
3
+
4
+ ### Linux ###
5
+ *~
6
+
7
+ # temporary files which can be created if a process still has a handle open of a deleted file
8
+ .fuse_hidden*
9
+
10
+ # KDE directory preferences
11
+ .directory
12
+
13
+ # Linux trash folder which might appear on any partition or disk
14
+ .Trash-*
15
+
16
+ # .nfs files are created when an open file is removed but is still being accessed
17
+ .nfs*
18
+
19
+ ### macOS ###
20
+ # General
21
+ .DS_Store
22
+ .AppleDouble
23
+ .LSOverride
24
+
25
+ # Icon must end with two \r
26
+ Icon
27
+
28
+
29
+ # Thumbnails
30
+ ._*
31
+
32
+ # Files that might appear in the root of a volume
33
+ .DocumentRevisions-V100
34
+ .fseventsd
35
+ .Spotlight-V100
36
+ .TemporaryItems
37
+ .Trashes
38
+ .VolumeIcon.icns
39
+ .com.apple.timemachine.donotpresent
40
+
41
+ # Directories potentially created on remote AFP share
42
+ .AppleDB
43
+ .AppleDesktop
44
+ Network Trash Folder
45
+ Temporary Items
46
+ .apdisk
47
+
48
+ ### macOS Patch ###
49
+ # iCloud generated files
50
+ *.icloud
51
+
52
+ ### Python ###
53
+ # Byte-compiled / optimized / DLL files
54
+ __pycache__/
55
+ *.py[cod]
56
+ *$py.class
57
+
58
+ # C extensions
59
+ *.so
60
+
61
+ # Distribution / packaging
62
+ .Python
63
+ build/
64
+ develop-eggs/
65
+ dist/
66
+ downloads/
67
+ eggs/
68
+ .eggs/
69
+ lib/
70
+ lib64/
71
+ parts/
72
+ sdist/
73
+ var/
74
+ wheels/
75
+ share/python-wheels/
76
+ *.egg-info/
77
+ .installed.cfg
78
+ *.egg
79
+ MANIFEST
80
+
81
+ # PyInstaller
82
+ # Usually these files are written by a python script from a template
83
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
84
+ *.manifest
85
+ *.spec
86
+
87
+ # Installer logs
88
+ pip-log.txt
89
+ pip-delete-this-directory.txt
90
+
91
+ # Unit test / coverage reports
92
+ htmlcov/
93
+ .tox/
94
+ .nox/
95
+ .coverage
96
+ .coverage.*
97
+ .cache
98
+ nosetests.xml
99
+ coverage.xml
100
+ *.cover
101
+ *.py,cover
102
+ .hypothesis/
103
+ .pytest_cache/
104
+ cover/
105
+
106
+ # Translations
107
+ *.mo
108
+ *.pot
109
+
110
+ # Django stuff:
111
+ *.log
112
+ local_settings.py
113
+ db.sqlite3
114
+ db.sqlite3-journal
115
+
116
+ # Flask stuff:
117
+ instance/
118
+ .webassets-cache
119
+
120
+ # Scrapy stuff:
121
+ .scrapy
122
+
123
+ # Sphinx documentation
124
+ docs/_build/
125
+
126
+ # PyBuilder
127
+ .pybuilder/
128
+ target/
129
+
130
+ # Jupyter Notebook
131
+ .ipynb_checkpoints
132
+
133
+ # IPython
134
+ profile_default/
135
+ ipython_config.py
136
+
137
+ # pyenv
138
+ # For a library or package, you might want to ignore these files since the code is
139
+ # intended to run in multiple environments; otherwise, check them in:
140
+ # .python-version
141
+
142
+ # pipenv
143
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
144
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
145
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
146
+ # install all needed dependencies.
147
+ #Pipfile.lock
148
+
149
+ # poetry
150
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
151
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
152
+ # commonly ignored for libraries.
153
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
154
+ #poetry.lock
155
+
156
+ # pdm
157
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
158
+ #pdm.lock
159
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
160
+ # in version control.
161
+ # https://pdm.fming.dev/#use-with-ide
162
+ .pdm.toml
163
+
164
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
165
+ __pypackages__/
166
+
167
+ # Celery stuff
168
+ celerybeat-schedule
169
+ celerybeat.pid
170
+
171
+ # SageMath parsed files
172
+ *.sage.py
173
+
174
+ # Environments
175
+ .env
176
+ .venv
177
+ env/
178
+ venv/
179
+ ENV/
180
+ env.bak/
181
+ venv.bak/
182
+
183
+ # Spyder project settings
184
+ .spyderproject
185
+ .spyproject
186
+
187
+ # Rope project settings
188
+ .ropeproject
189
+
190
+ # mkdocs documentation
191
+ /site
192
+
193
+ # mypy
194
+ .mypy_cache/
195
+ .dmypy.json
196
+ dmypy.json
197
+
198
+ # Pyre type checker
199
+ .pyre/
200
+
201
+ # pytype static type analyzer
202
+ .pytype/
203
+
204
+ # Cython debug symbols
205
+ cython_debug/
206
+
207
+ # PyCharm
208
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
209
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
210
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
211
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
212
+ #.idea/
213
+
214
+ ### Python Patch ###
215
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
216
+ poetry.toml
217
+
218
+ # ruff
219
+ .ruff_cache/
220
+
221
+ # LSP config files
222
+ pyrightconfig.json
223
+
224
+ ### Windows ###
225
+ # Windows thumbnail cache files
226
+ Thumbs.db
227
+ Thumbs.db:encryptable
228
+ ehthumbs.db
229
+ ehthumbs_vista.db
230
+
231
+ # Dump file
232
+ *.stackdump
233
+
234
+ # Folder config file
235
+ [Dd]esktop.ini
236
+
237
+ # Recycle Bin used on file shares
238
+ $RECYCLE.BIN/
239
+
240
+ # Windows Installer files
241
+ *.cab
242
+ *.msi
243
+ *.msix
244
+ *.msm
245
+ *.msp
246
+
247
+ # Windows shortcuts
248
+ *.lnk
249
+
250
+ # End of https://www.toptal.com/developers/gitignore/api/python,linux,windows,macos
251
+
252
+
253
+ # Custom additions
254
+
255
+ # Editor swap and temporary files
256
+ *.swp
257
+ *.swo
258
+ *.tmp
259
+ .pyproject.toml.sw*
devtkit-0.1.1/LICENSE ADDED
@@ -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.
devtkit-0.1.1/PKG-INFO ADDED
@@ -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,127 @@
1
+ # devtkit
2
+
3
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue)](https://github.com/tyleruploads/devtkit/blob/main/LICENSE)
4
+ [![GitHub issues](https://img.shields.io/github/issues/tyleruploads/devtkit)](https://github.com/tyleruploads/devtkit/issues)
5
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
6
+
7
+ [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.
8
+
9
+ ## Installation
10
+ Currently, there are 2 ways to install devtkit
11
+
12
+ <details>
13
+ <summary>Universal (Windows, Linux, and macOS)</summary>
14
+ <br>
15
+
16
+ ### Install pipx (if you do not have it)
17
+ * **Windows**:
18
+ ```cmd
19
+ pip install pipx
20
+ pipx ensurepath
21
+ ```
22
+ * **Linux (Debian/Ubuntu):**
23
+ ```bash
24
+ sudo apt update
25
+ sudo apt install pipx
26
+ pipx ensurepath
27
+ ```
28
+ * **Linux (Fedora):**
29
+ ```bash
30
+ sudo dnf install pipx
31
+ pipx ensurepath
32
+ ```
33
+ * **macOS (via [Homebrew](https://brew.sh/)):**
34
+ ```bash
35
+ brew install pipx
36
+ pipx ensurepath
37
+ ```
38
+
39
+ ### Installing the CLI Utility
40
+ Now that you have installed `pipx`, run the following command to install [`devtkit`](https://github.com/tyleruploads/devtkit):
41
+ ```bash
42
+ pipx install devtkit
43
+ ```
44
+ </details>
45
+
46
+ <details>
47
+ <summary>Clone the repository and run the script</summary>
48
+ <br>
49
+
50
+ If you want to run the script, view the source code, or contribute, clone the repository locally:
51
+
52
+ ```bash
53
+ git clone https://github.com/tyleruploads/devtkit.git
54
+ cd devtkit
55
+
56
+ pip install -r requirements.txt
57
+ ```
58
+
59
+ > If you are getting a PEP 668 error, ensure you have [`pipx`](https://pipx.pypa.io/stable/) installed and run:
60
+ > `pipx install --editable .`
61
+
62
+ **If you installed with pipx**:
63
+ Run `devtkit`
64
+
65
+ **Otherwise:**
66
+ Run `python3 src/main.py`
67
+
68
+
69
+ </details>
70
+
71
+
72
+ ## Usage
73
+ To use devtkit, simply follow along with the prompts the script gives you.
74
+
75
+ > To get a DEV.to API Key, navigate to: DEV.to -> Settings -> Extensions, and scroll till you find "DEV Community API Keys"
76
+ > 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)
77
+
78
+ An example run is:
79
+
80
+ ```text
81
+ --- Formats ---
82
+
83
+ 0. Markdown
84
+ 1. CSV
85
+ 2. JSON
86
+
87
+ Please enter the numbers for the following formats you would like to save to: 012
88
+ --- File Save Locations ---
89
+ Please enter save path for Markdown (Default: followers.md): ~/Documents/followers.md
90
+ Please enter save path for Csv (Default: followers.csv): ~/Documents/followers.csv
91
+ Please enter save path for Json (Default: followers.json): ~/Documents/followers.json
92
+
93
+ To get an API Key, go to: DEV.to -> Settings -> Extensions, and scroll to the bottom.
94
+ DEV.to API Key: (securely collected with the getpass module from the Python STL)
95
+ Followers to pull in each GET request (default is 1000):
96
+
97
+ A maximum of 1000 users will be pulled from each page.
98
+
99
+ Page count: 1.
100
+ 534 followers pulled on page 1. 534 total followers have been found so far.
101
+
102
+ Page count: 2.
103
+ 0 followers pulled on page 2. 534 total followers found.
104
+
105
+ Saved in the Markdown file format to /home/tyler/Documents/followers.md
106
+ Saved in the CSV file format to /home/tyler/Documents/followers.csv
107
+ Saved in the JSON file format to /home/tyler/Documents/followers.json
108
+ ```
109
+
110
+ ## Features
111
+ devtkit has a large array of features that make it stand out from projects like it.
112
+
113
+ * **Multi-Format Export**: Save to Markdown, CSV, and JSON files
114
+ * **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
115
+ * **Smart Rate-Limiting**: Automatically handles `429 Too Many Requests` responses
116
+ * **Beautiful and Detailed Output**: Outputs a beautiful and detailed Markdown file, a detailed CSV or JSON file, or all 3
117
+
118
+ ## Contributing
119
+
120
+ Contributions are what make open-source projects important. All contributions are highly appreciated
121
+
122
+ * **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
123
+ * **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.
124
+
125
+ ## License
126
+
127
+ [MIT](https://choosealicense.com/licenses/mit/)
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "devtkit"
7
+ dynamic = ["version"]
8
+ description = "An open-source DEV Community toolkit written in Python that lets users export information gathered from the DEV.to API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "tyleruploads"}
14
+ ]
15
+ keywords = ["python", "markdown", "pagination", "cli", "open-source", "security", "json", "utility", "automation", "csv", "rest-api", "scripting", "python-script", "rate-limiting", "developer-tools", "data-export", "api-integration", "devto", "dev-to", "forem"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent"
20
+ ]
21
+ dependencies = [
22
+ "requests>=2.31.0"
23
+ ]
24
+
25
+ [project.scripts]
26
+ devtkit = "devtkit.main:main"
27
+
28
+ [tool.hatch.version]
29
+ path = "src/main.py"
30
+
31
+ [tool.hatch.build.targets.wheel]
32
+ packages = ["src"]
33
+
34
+ [tool.hatch.build.targets.wheel.sources]
35
+ "src" = "devtkit"
@@ -0,0 +1 @@
1
+ requests==2.32.3
@@ -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()