devto-followers2md 0.1.0__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/devto-followers2md
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,250 @@
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
@@ -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.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: devto-followers2md
3
+ Version: 0.1.0
4
+ Summary: An open-source Python program that exports information about a user's followers on DEV.to in a beautiful Markdown file or complete CSV or JSON file.
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
+ # devto-followers2md
17
+
18
+ [![GitHub license](https://img.shields.io/github/license/tyleruploads/devto-followers2md)](https://github.com/tyleruploads/devto-followers2md/blob/main/LICENSE)
19
+ [![GitHub issues](https://img.shields.io/github/issues/tyleruploads/devto-followers2md)](https://github.com/tyleruploads/devto-followers2md/issues)
20
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
21
+
22
+ DEV.to Followers to Markdown (devto-followers2md) is an open-source Python program that exports information about a user's followers on DEV.to in a beautiful Markdown file or complete CSV or JSON file.
23
+
24
+ ## Installation
25
+ Currently, the way to run the script is to clone the repository and run the file located at src/main.py
26
+
27
+ <details>
28
+ <summary>Clone the repository and run the script</summary>
29
+ <br>
30
+
31
+ If you want to run the script, view the soruce code, or contribute, clone the repository locally:
32
+
33
+ ```bash
34
+ git clone https://github.com/tyleruploads/devto-followers2md.git
35
+ cd devto-followers2md/src
36
+ python3 main.py
37
+ ```
38
+
39
+ Consult the `requirements.txt` file in the root of the project to ensure you have all the required dependencies.
40
+ </details>
41
+
42
+
43
+ ## Usage
44
+ To use devto-followers2md, simply follow along with the prompts the script gives you.
45
+
46
+ > To get a DEV.to API Key, navigate to: DEV.to -> Settings -> Extensions, and scroll till you find "DEV Community API Keys"
47
+ > 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)
48
+
49
+ An example run is:
50
+
51
+ ```text
52
+ --- Formats ---
53
+
54
+ 0. Markdown
55
+ 1. CSV
56
+ 2. JSON
57
+
58
+ Please enter the numbers for the following formats you would like to save to: 012
59
+ --- File Save Locations ---
60
+ Please enter save path for Markdown (Default: followers.md): ~/Documents/followers.md
61
+ Please enter save path for Csv (Default: followers.csv): ~/Documents/followers.csv
62
+ Please enter save path for Json (Default: followers.json): ~/Documents/followers.json
63
+
64
+ To get an API Key, go to: DEV.to -> Settings -> Extensions, and scroll to the bottom.
65
+ DEV.to API Key: (securely collected with the getpass module from the Python STL)
66
+ Followers to pull in each GET request (default is 1000):
67
+
68
+ A maximum of 1000 users will be pulled from each page.
69
+
70
+ Page count: 1.
71
+ 534 followers pulled on page 1. 534 total followers have been found so far.
72
+
73
+ Page count: 2.
74
+ 0 followers pulled on page 2. 534 total followers found.
75
+
76
+ Saved in the Markdown file format to /home/tyler/Documents/followers.md
77
+ Saved in the CSV file format to /home/tyler/Documents/followers.csv
78
+ Saved in the JSON file format to /home/tyler/Documents/followers.json
79
+ ```
80
+
81
+ ## Features
82
+ devto-followers2md has a large array of features that make it stand out from projects like it.
83
+
84
+ * **Multi-Format Export**: Save to Markdown, CSV, and JSON files
85
+ * **Secure API Key Handling**: Securely collects the users DEV.to API Key with the Python STL Module getpass and only uses it to interact with the DEV.to API endpoint
86
+ * **Smart Rate-Limiting**: Automatically handles `429 Too Many Requests` responses
87
+ * **Beautiful and Detailed Output**: Outputs a beautiful and detailed Markdown file, a detailed CSV or JSON file, or all 3
88
+
89
+ ## Contibuting
90
+
91
+ Contibutions are what make open-source projects important. All contributions are highly appreciated
92
+
93
+ * **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
94
+ * **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.
95
+
96
+ ## License
97
+
98
+ [MIT](https://choosealicense.com/licenses/mit/)
@@ -0,0 +1,83 @@
1
+ # devto-followers2md
2
+
3
+ [![GitHub license](https://img.shields.io/github/license/tyleruploads/devto-followers2md)](https://github.com/tyleruploads/devto-followers2md/blob/main/LICENSE)
4
+ [![GitHub issues](https://img.shields.io/github/issues/tyleruploads/devto-followers2md)](https://github.com/tyleruploads/devto-followers2md/issues)
5
+ [![Python Version](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
6
+
7
+ DEV.to Followers to Markdown (devto-followers2md) is an open-source Python program that exports information about a user's followers on DEV.to in a beautiful Markdown file or complete CSV or JSON file.
8
+
9
+ ## Installation
10
+ Currently, the way to run the script is to clone the repository and run the file located at src/main.py
11
+
12
+ <details>
13
+ <summary>Clone the repository and run the script</summary>
14
+ <br>
15
+
16
+ If you want to run the script, view the soruce code, or contribute, clone the repository locally:
17
+
18
+ ```bash
19
+ git clone https://github.com/tyleruploads/devto-followers2md.git
20
+ cd devto-followers2md/src
21
+ python3 main.py
22
+ ```
23
+
24
+ Consult the `requirements.txt` file in the root of the project to ensure you have all the required dependencies.
25
+ </details>
26
+
27
+
28
+ ## Usage
29
+ To use devto-followers2md, simply follow along with the prompts the script gives you.
30
+
31
+ > To get a DEV.to API Key, navigate to: DEV.to -> Settings -> Extensions, and scroll till you find "DEV Community API Keys"
32
+ > 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)
33
+
34
+ An example run is:
35
+
36
+ ```text
37
+ --- Formats ---
38
+
39
+ 0. Markdown
40
+ 1. CSV
41
+ 2. JSON
42
+
43
+ Please enter the numbers for the following formats you would like to save to: 012
44
+ --- File Save Locations ---
45
+ Please enter save path for Markdown (Default: followers.md): ~/Documents/followers.md
46
+ Please enter save path for Csv (Default: followers.csv): ~/Documents/followers.csv
47
+ Please enter save path for Json (Default: followers.json): ~/Documents/followers.json
48
+
49
+ To get an API Key, go to: DEV.to -> Settings -> Extensions, and scroll to the bottom.
50
+ DEV.to API Key: (securely collected with the getpass module from the Python STL)
51
+ Followers to pull in each GET request (default is 1000):
52
+
53
+ A maximum of 1000 users will be pulled from each page.
54
+
55
+ Page count: 1.
56
+ 534 followers pulled on page 1. 534 total followers have been found so far.
57
+
58
+ Page count: 2.
59
+ 0 followers pulled on page 2. 534 total followers found.
60
+
61
+ Saved in the Markdown file format to /home/tyler/Documents/followers.md
62
+ Saved in the CSV file format to /home/tyler/Documents/followers.csv
63
+ Saved in the JSON file format to /home/tyler/Documents/followers.json
64
+ ```
65
+
66
+ ## Features
67
+ devto-followers2md has a large array of features that make it stand out from projects like it.
68
+
69
+ * **Multi-Format Export**: Save to Markdown, CSV, and JSON files
70
+ * **Secure API Key Handling**: Securely collects the users DEV.to API Key with the Python STL Module getpass and only uses it to interact with the DEV.to API endpoint
71
+ * **Smart Rate-Limiting**: Automatically handles `429 Too Many Requests` responses
72
+ * **Beautiful and Detailed Output**: Outputs a beautiful and detailed Markdown file, a detailed CSV or JSON file, or all 3
73
+
74
+ ## Contibuting
75
+
76
+ Contibutions are what make open-source projects important. All contributions are highly appreciated
77
+
78
+ * **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
79
+ * **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.
80
+
81
+ ## License
82
+
83
+ [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 = "devto-followers2md"
7
+ dynamic = ["version"]
8
+ description = "An open-source Python program that exports information about a user's followers on DEV.to in a beautiful Markdown file or complete CSV or JSON file."
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
+ devto-followers2md = "devto_followers2md.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" = "devto_followers2md"
@@ -0,0 +1 @@
1
+ requests==2.32.3
@@ -0,0 +1,335 @@
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.0"
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
+ devto-followers2md (v{__version__})
24
+ Repository: https://github.com/tyleruploads/devto-followers2md
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/devto-followers2md/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 409 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
+ # devto-followers2md \n
226
+ > Generated by [devto-followers2md](https://github.com/tyleruploads/devto-followers2md).
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"| <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: list[dict], 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
+ save_files(followers_list, formats_and_paths, self_info)
333
+
334
+ if __name__ == "__main__":
335
+ main()