├── .gitignore
├── LICENSE
├── README.md
├── poetry.lock
├── pydevto.egg-info
├── PKG-INFO
├── SOURCES.txt
├── dependency_links.txt
├── requires.txt
└── top_level.txt
├── pydevto
├── __init__.py
├── markdown_converter.py
├── markdownify_license.txt
└── pydevto.py
├── pyproject.toml
└── tests
├── __init__.py
└── test_pydevto.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # files
2 | .DS_Store
3 | test.py
4 |
5 | # folders
6 | .idea
7 | __pycache__
8 | pytest_cache
9 | how_long.egg-info
10 | pip-wheel-metadata
11 | dist
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Loftie Ellis
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyDevTo
2 |
3 | Unofficial dev.to api for python.
4 |
5 | ### Features
6 | * Implements all endpoints from https://docs.dev.to/api/
7 | * Implements a few other api endpoints not documented but available in the source, such as users and follow_suggestions
8 | * Includes a helper method to convert html to dev.to specific markdown, including support for dev.to specific embeds such as YouTube.
9 |
10 |
11 | ## Installation
12 |
13 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install pydevto.
14 |
15 | ```bash
16 | pip install pydevto
17 | ```
18 |
19 | ## Usage
20 |
21 | Make sure you have an api key to use the authenticated endpoints. You can get your key from https://dev.to/settings/account
22 | (You can use pydevto without an api key for some functions, such as the public articles)
23 |
24 | ```python
25 | import pydevto
26 | api = pydevto.PyDevTo(api_key='MY_KEY')
27 | api.articles() # returns list of your own published articles
28 | ```
29 |
30 | ## Methods
31 | ```python
32 | import pydevto
33 | api = pydevto.PyDevTo(api_key='MY_KEY')
34 | api.public_articles(page=None, tag=None, username=None, state=None, top=None) # Return list of public (published) articles
35 | api.public_article(id) # Return a single public (published) article given its id
36 | api.articles(page=None, per_page=None, state="published") # Return a list of user articles
37 | api.create_article(...) # Create an article
38 | api.update_article(id, ...) # Update an article
39 | api.user(id=None, username=None) # Return user information
40 | api.follow_suggestions(page=None) # Return list of follow suggestions
41 | api.tags(page=None) # Return list of tags
42 | api.webhooks() # Return list of webhooks
43 | api.webhook(id) # Return single webhook with id
44 | api.create_webhook(source, target_url, events) # Create a new webhook
45 | api.delete_webhook(id) # Delete a webhook with id
46 | ```
47 |
48 | ## Html to Markdown
49 | PyDevTo contains a helper function to convert html to dev.to specific markdown (https://dev.to/p/editor_guide)
50 | It supports images with captions using the HTML figcaption tag, and converts embeds such as YouTube to dev.to specific liquid tags.
51 | ```python
52 | >>> import pydevto
53 | >>> pydevto.html_to_markdown('
Heading
>> '# Heading\n\n'
55 | >>> pydevto.html_to_markdown('')
56 | >>> '\n{% youtube kmjiUVEMvI4 %}\n'
57 | ```
58 |
59 | ## Known issues
60 | * The tags property does not currently work correctly when creating/updating an article. There is an open issue report on dev.to for this.
61 | * The html to markdown only caters for a subset of embeds (YouTube, Twitter, repl.it, soundcloud and a few more), more will be added over time.
62 |
63 | ## Contributing
64 | Pull requests and issue reports are welcome.
65 |
66 | ## License
67 | [MIT](https://choosealicense.com/licenses/mit/)
68 |
--------------------------------------------------------------------------------
/poetry.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | category = "dev"
3 | description = "Atomic file writes."
4 | name = "atomicwrites"
5 | optional = false
6 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
7 | version = "1.3.0"
8 |
9 | [[package]]
10 | category = "dev"
11 | description = "Classes Without Boilerplate"
12 | name = "attrs"
13 | optional = false
14 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
15 | version = "19.3.0"
16 |
17 | [[package]]
18 | category = "main"
19 | description = "Screen-scraping library"
20 | name = "beautifulsoup4"
21 | optional = false
22 | python-versions = "*"
23 | version = "4.8.1"
24 |
25 | [package.dependencies]
26 | soupsieve = ">=1.2"
27 |
28 | [[package]]
29 | category = "main"
30 | description = "Python package for providing Mozilla's CA Bundle."
31 | name = "certifi"
32 | optional = false
33 | python-versions = "*"
34 | version = "2019.9.11"
35 |
36 | [[package]]
37 | category = "main"
38 | description = "Universal encoding detector for Python 2 and 3"
39 | name = "chardet"
40 | optional = false
41 | python-versions = "*"
42 | version = "3.0.4"
43 |
44 | [[package]]
45 | category = "dev"
46 | description = "Cross-platform colored terminal text."
47 | marker = "sys_platform == \"win32\""
48 | name = "colorama"
49 | optional = false
50 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
51 | version = "0.4.1"
52 |
53 | [[package]]
54 | category = "main"
55 | description = "Internationalized Domain Names in Applications (IDNA)"
56 | name = "idna"
57 | optional = false
58 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
59 | version = "2.8"
60 |
61 | [[package]]
62 | category = "dev"
63 | description = "Read metadata from Python packages"
64 | marker = "python_version < \"3.8\""
65 | name = "importlib-metadata"
66 | optional = false
67 | python-versions = ">=2.7,!=3.0,!=3.1,!=3.2,!=3.3"
68 | version = "0.23"
69 |
70 | [package.dependencies]
71 | zipp = ">=0.5"
72 |
73 | [[package]]
74 | category = "dev"
75 | description = "More routines for operating on iterables, beyond itertools"
76 | name = "more-itertools"
77 | optional = false
78 | python-versions = ">=3.4"
79 | version = "7.2.0"
80 |
81 | [[package]]
82 | category = "dev"
83 | description = "plugin and hook calling mechanisms for python"
84 | name = "pluggy"
85 | optional = false
86 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
87 | version = "0.13.0"
88 |
89 | [package.dependencies]
90 | [package.dependencies.importlib-metadata]
91 | python = "<3.8"
92 | version = ">=0.12"
93 |
94 | [[package]]
95 | category = "dev"
96 | description = "library with cross-python path, ini-parsing, io, code, log facilities"
97 | name = "py"
98 | optional = false
99 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
100 | version = "1.8.0"
101 |
102 | [[package]]
103 | category = "dev"
104 | description = "pytest: simple powerful testing with Python"
105 | name = "pytest"
106 | optional = false
107 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
108 | version = "3.10.1"
109 |
110 | [package.dependencies]
111 | atomicwrites = ">=1.0"
112 | attrs = ">=17.4.0"
113 | colorama = "*"
114 | more-itertools = ">=4.0.0"
115 | pluggy = ">=0.7"
116 | py = ">=1.5.0"
117 | setuptools = "*"
118 | six = ">=1.10.0"
119 |
120 | [[package]]
121 | category = "main"
122 | description = "Python HTTP for Humans."
123 | name = "requests"
124 | optional = false
125 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
126 | version = "2.22.0"
127 |
128 | [package.dependencies]
129 | certifi = ">=2017.4.17"
130 | chardet = ">=3.0.2,<3.1.0"
131 | idna = ">=2.5,<2.9"
132 | urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
133 |
134 | [[package]]
135 | category = "main"
136 | description = "Python 2 and 3 compatibility utilities"
137 | name = "six"
138 | optional = false
139 | python-versions = ">=2.6, !=3.0.*, !=3.1.*"
140 | version = "1.12.0"
141 |
142 | [[package]]
143 | category = "main"
144 | description = "A modern CSS selector implementation for Beautiful Soup."
145 | name = "soupsieve"
146 | optional = false
147 | python-versions = "*"
148 | version = "1.9.4"
149 |
150 | [[package]]
151 | category = "main"
152 | description = "HTTP library with thread-safe connection pooling, file post, and more."
153 | name = "urllib3"
154 | optional = false
155 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
156 | version = "1.25.6"
157 |
158 | [[package]]
159 | category = "dev"
160 | description = "Backport of pathlib-compatible object wrapper for zip files"
161 | marker = "python_version < \"3.8\""
162 | name = "zipp"
163 | optional = false
164 | python-versions = ">=2.7"
165 | version = "0.6.0"
166 |
167 | [package.dependencies]
168 | more-itertools = "*"
169 |
170 | [metadata]
171 | content-hash = "3830f2ec9500abfc74d668704848cef59aed7b2e11b9068282250c624b699fcb"
172 | python-versions = "^3.6"
173 |
174 | [metadata.hashes]
175 | atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", "75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"]
176 | attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
177 | beautifulsoup4 = ["5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169", "6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931", "dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57"]
178 | certifi = ["e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", "fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"]
179 | chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"]
180 | colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
181 | idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"]
182 | importlib-metadata = ["aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26", "d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"]
183 | more-itertools = ["409cd48d4db7052af495b09dec721011634af3753ae1ef92d2b32f73a745f832", "92b8c4b06dac4f0611c0729b2f2ede52b2e1bac1ab48f089c7ddc12e26bb60c4"]
184 | pluggy = ["0db4b7601aae1d35b4a033282da476845aa19185c1e6964b25cf324b5e4ec3e6", "fa5fa1622fa6dd5c030e9cad086fa19ef6a0cf6d7a2d12318e10cb49d6d68f34"]
185 | py = ["64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", "dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53"]
186 | pytest = ["3f193df1cfe1d1609d4c583838bea3d532b18d6160fd3f55c9447fdca30848ec", "e246cf173c01169b9617fc07264b7b1316e78d7a650055235d6d897bc80d9660"]
187 | requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
188 | six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
189 | soupsieve = ["605f89ad5fdbfefe30cdc293303665eff2d188865d4dbe4eb510bba1edfbfce3", "b91d676b330a0ebd5b21719cb6e9b57c57d433671f65b9c28dd3461d9a1ed0b6"]
190 | urllib3 = ["3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398", "9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86"]
191 | zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"]
192 |
--------------------------------------------------------------------------------
/pydevto.egg-info/PKG-INFO:
--------------------------------------------------------------------------------
1 | Metadata-Version: 1.2
2 | Name: pydevto
3 | Version: 0.1.3
4 | Summary: Unofficial dev.to api for python
5 | Home-page: https://github.com/lpellis/pydevto
6 | Author: 'Loftie
7 | Author-email: lpellis@gmail.com
8 | License: UNKNOWN
9 | Description: # PyDevTo
10 |
11 | Unofficial dev.to api for python.
12 |
13 | ### Features
14 | * Implements all endpoints from https://docs.dev.to/api/
15 | * Implements a few other api endpoints not documented but available in the source, such as users and follow_suggestions
16 | * Includes a helper method to convert html to dev.to specific markdown, including support for dev.to specific embeds such as YouTube.
17 |
18 |
19 | ## Installation
20 |
21 | Use the package manager [pip](https://pip.pypa.io/en/stable/) to install pydevto.
22 |
23 | ```bash
24 | pip install pydevto
25 | ```
26 |
27 | ## Usage
28 |
29 | Make sure you have an api key to use the authenticated endpoints. You can get your key from https://dev.to/settings/account
30 | (You can use pydevto without an api key for some functions, such as the public articles)
31 |
32 | ```python
33 | import pydevto
34 | api = pydevto.PyDevTo(api_key='MY_KEY')
35 | api.articles() # returns list of your own published articles
36 | ```
37 |
38 | ## Methods
39 | ```python
40 | import pydevto
41 | api = pydevto.PyDevTo(api_key='MY_KEY')
42 | api.public_articles(page=None, tag=None, username=None, state=None, top=None) # Return list of public (published) articles
43 | api.public_article(id) # Return a single public (published) article given its id
44 | api.articles(page=None, per_page=None, state="published") # Return a list of user articles
45 | api.create_article(...) # Create an article
46 | api.update_article(id, ...) # Update an article
47 | api.user(id=None, username=None) # Return user information
48 | api.follow_suggestions(page=None) # Return list of follow suggestions
49 | api.tags(page=None) # Return list of tags
50 | api.webhooks() # Return list of webhooks
51 | api.webhook(id) # Return single webhook with id
52 | api.create_webhook(source, target_url, events) # Create a new webhook
53 | api.delete_webhook(id) # Delete a webhook with id
54 | ```
55 |
56 | ## Html to Markdown
57 | PyDevTo contains a helper function to convert html to dev.to specific markdown (https://dev.to/p/editor_guide)
58 | It supports images with captions using the HTML figcaption tag, and converts embeds such as YouTube to dev.to specific liquid tags.
59 | ```python
60 | >>> import pydevto
61 | >>> pydevto.html_to_markdown('
Heading
>> '# Heading\n\n'
63 | >>> pydevto.html_to_markdown('')
64 | >>> '\n{% youtube kmjiUVEMvI4 %}\n'
65 | ```
66 |
67 | ## Known issues
68 | * The tags property does not currently work correctly when creating/updating an article. There is an open issue report on dev.to for this.
69 | * The html to markdown only caters for a subset of embeds (YouTube, Twitter, repl.it, soundcloud and a few more), more will be added over time.
70 |
71 | ## Contributing
72 | Pull requests and issue reports are welcome.
73 |
74 | ## License
75 | [MIT](https://choosealicense.com/licenses/mit/)
76 |
77 | Platform: UNKNOWN
78 | Requires-Python: >=3.6,<4.0
79 |
--------------------------------------------------------------------------------
/pydevto.egg-info/SOURCES.txt:
--------------------------------------------------------------------------------
1 | README.md
2 | setup.py
3 | pydevto/__init__.py
4 | pydevto/markdown_converter.py
5 | pydevto/markdownify_license.txt
6 | pydevto/pydevto.py
7 | pydevto.egg-info/PKG-INFO
8 | pydevto.egg-info/SOURCES.txt
9 | pydevto.egg-info/dependency_links.txt
10 | pydevto.egg-info/requires.txt
11 | pydevto.egg-info/top_level.txt
--------------------------------------------------------------------------------
/pydevto.egg-info/dependency_links.txt:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/pydevto.egg-info/requires.txt:
--------------------------------------------------------------------------------
1 | beautifulsoup4<5.0,>=4.8
2 | requests<3.0,>=2.22
3 | six<2.0,>=1.12
4 |
--------------------------------------------------------------------------------
/pydevto.egg-info/top_level.txt:
--------------------------------------------------------------------------------
1 | pydevto
2 |
--------------------------------------------------------------------------------
/pydevto/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = '0.1.0'
2 |
3 | from pydevto.pydevto import PyDevTo
4 | from pydevto.markdown_converter import html_to_markdown
5 |
--------------------------------------------------------------------------------
/pydevto/markdown_converter.py:
--------------------------------------------------------------------------------
1 | import urllib.parse as urlparse
2 |
3 | from bs4 import BeautifulSoup, NavigableString
4 | import re
5 | import six
6 |
7 |
8 | convert_heading_re = re.compile(r"convert_h(\d+)")
9 | line_beginning_re = re.compile(r"^", re.MULTILINE)
10 | whitespace_re = re.compile(r"[\r\n\s\t ]+")
11 | FRAGMENT_ID = "__MARKDOWNIFY_WRAPPER__"
12 | wrapped = '
%%s
' % FRAGMENT_ID
13 |
14 |
15 | # Heading styles
16 | ATX = "atx"
17 | ATX_CLOSED = "atx_closed"
18 | UNDERLINED = "underlined"
19 | SETEXT = UNDERLINED
20 |
21 |
22 | def escape(text):
23 | if not text:
24 | return ""
25 | return text.replace("_", r"\_")
26 |
27 |
28 | def _todict(obj):
29 | return dict((k, getattr(obj, k)) for k in dir(obj) if not k.startswith("_"))
30 |
31 |
32 | class MarkdownConverter(object):
33 | """
34 | This class is based on on markdownify (https://github.com/matthewwithanm/python-markdownify) but extended to
35 | handle dev.to specific markdown and embeds
36 | """
37 | class DefaultOptions:
38 | strip = None
39 | convert = None
40 | autolinks = True
41 | heading_style = UNDERLINED
42 | bullets = "*+-" # An iterable of bullet types.
43 |
44 | class Options(DefaultOptions):
45 | pass
46 |
47 | def __init__(self, **options):
48 | # Create an options dictionary. Use DefaultOptions as a base so that
49 | # it doesn't have to be extended.
50 | self.options = _todict(self.DefaultOptions)
51 | self.options.update(_todict(self.Options))
52 | self.options.update(options)
53 | if self.options["strip"] is not None and self.options["convert"] is not None:
54 | raise ValueError(
55 | "You may specify either tags to strip or tags to"
56 | " convert, but not both."
57 | )
58 |
59 | def convert(self, html):
60 | # We want to take advantage of the html5 parsing, but we don't actually
61 | # want a full document. Therefore, we'll mark our fragment with an id,
62 | # create the document, and extract the element with the id.
63 | html = wrapped % html
64 | soup = BeautifulSoup(html, "html.parser")
65 | return self.process_tag(soup.find(id=FRAGMENT_ID), children_only=True)
66 |
67 | def process_tag(self, node, children_only=False):
68 | text = ""
69 |
70 | # Convert the children first
71 | for el in node.children:
72 | if isinstance(el, NavigableString):
73 | text += self.process_text(six.text_type(el))
74 | else:
75 | text += self.process_tag(el)
76 |
77 | if not children_only:
78 | convert_fn = getattr(self, "convert_%s" % node.name, None)
79 | if convert_fn and self.should_convert_tag(node.name):
80 | text = convert_fn(node, text)
81 | # else:
82 | # text = text + node.name
83 |
84 | return text
85 |
86 | def process_text(self, text):
87 | return escape(whitespace_re.sub(" ", text or ""))
88 |
89 | def __getattr__(self, attr):
90 | # Handle headings
91 | m = convert_heading_re.match(attr)
92 | if m:
93 | n = int(m.group(1))
94 |
95 | def convert_tag(el, text):
96 | return self.convert_hn(n, el, text)
97 |
98 | convert_tag.__name__ = "convert_h%s" % n
99 | setattr(self, convert_tag.__name__, convert_tag)
100 | return convert_tag
101 |
102 | raise AttributeError(attr)
103 |
104 | def should_convert_tag(self, tag):
105 | tag = tag.lower()
106 | strip = self.options["strip"]
107 | convert = self.options["convert"]
108 | if strip is not None:
109 | return tag not in strip
110 | elif convert is not None:
111 | return tag in convert
112 | else:
113 | return True
114 |
115 | def indent(self, text, level):
116 | return line_beginning_re.sub("\t" * level, text) if text else ""
117 |
118 | def underline(self, text, pad_char):
119 | text = (text or "").rstrip()
120 | return "%s\n%s\n\n" % (text, pad_char * len(text)) if text else ""
121 |
122 | def convert_a(self, el, text):
123 | href = el.get("href")
124 | title = el.get("title")
125 | if self.options["autolinks"] and text == href and not title:
126 | # Shortcut syntax
127 | return "<%s>" % href
128 | title_part = ' "%s"' % title.replace('"', r"\"") if title else ""
129 | return "[%s](%s%s)" % (text or "", href, title_part) if href else text or ""
130 |
131 | def convert_b(self, el, text):
132 | return self.convert_strong(el, text)
133 |
134 | def convert_figcaption(self, el, text):
135 | return "\n" + text + "\n"
136 |
137 | def convert_blockquote(self, el, text):
138 |
139 | # handle twitter embeds
140 | if el.get("class") and el.get("class")[0] == "twitter-tweet":
141 | for entry in el.find_all("a"):
142 | if entry.get("href").startswith("https://twitter.com/"):
143 | return self.pydevto_embed(entry.get("href"))
144 |
145 | return "\n" + line_beginning_re.sub("> ", text) if text else ""
146 |
147 | def convert_br(self, el, text):
148 | return " \n"
149 |
150 | def remove_scheme(self, url):
151 | return (
152 | url.replace("https://www.", "//")
153 | .replace("https://", "//")
154 | .replace("http://www.", "//")
155 | .replace("http://", "//")
156 | )
157 |
158 | def convert_iframe(self, el, text):
159 | src = self.remove_scheme(el.attrs.get("src"))
160 | if src.startswith("//cdn.unfurl.dev/embed?"):
161 | src = urlparse.unquote(src)
162 | query = urlparse.urlsplit(src).query
163 | params = urlparse.parse_qs(query)
164 | if params.get("url"):
165 | embed = params.get("url")[0]
166 | return self.pydevto_embed(embed)
167 |
168 | if src.startswith("//cdn.embedly.com"):
169 | src = urlparse.unquote(src)
170 | query = urlparse.urlsplit(src).query
171 | params = urlparse.parse_qs(query)
172 | if params.get("src"):
173 | embed = params.get("src")[0]
174 | return self.pydevto_embed(embed)
175 |
176 | return self.pydevto_embed(el.attrs.get("src"))
177 |
178 | def pydevto_embed(self, url):
179 | original_url = url
180 | url = self.remove_scheme(url)
181 |
182 | if url.startswith("//twitter.com"):
183 | q = urlparse.urlsplit(url)
184 | return "\n{% twitter " + q.path.split("/")[-1] + " %}\n"
185 |
186 | if url.startswith("//youtube.com"):
187 | q = urlparse.urlsplit(url)
188 | if q.path.startswith("/embed/"):
189 | return "\n{% youtube " + q.path.replace("/embed/", "") + " %}\n"
190 | v = urlparse.parse_qs(q.query)["v"][0]
191 | return "\n{% youtube " + v + " %}\n"
192 |
193 | if url.startswith("//codepen.io"):
194 | return "\n{% codepen " + original_url + " %}\n"
195 |
196 | if url.startswith("//soundcloud.com"):
197 | return "\n{% soundcloud " + original_url + " %}\n"
198 |
199 | if url.startswith("//github.com"):
200 | return "\n{% github " + original_url + " %}\n"
201 |
202 | if url.startswith("//instagram.com"):
203 | q = urlparse.urlsplit(url)
204 | return (
205 | "\n{% instagram " + q.path.replace("/p", "").replace("/", "") + " %}\n"
206 | )
207 |
208 | if url.startswith("//repl.it"):
209 | q = urlparse.urlsplit(url)
210 | return "\n{% replit " + q.path[1:] + " %}\n"
211 |
212 | return "\n" + original_url + "\n"
213 |
214 | def convert_em(self, el, text):
215 | return "*%s*" % text if text else ""
216 |
217 | def convert_hn(self, n, el, text):
218 | style = self.options["heading_style"]
219 | text = text.rstrip()
220 | if style == UNDERLINED and n <= 2:
221 | line = "=" if n == 1 else "-"
222 | return self.underline(text, line)
223 | hashes = "#" * n
224 | if style == ATX_CLOSED:
225 | return "%s %s %s\n\n" % (hashes, text, hashes)
226 | return "%s %s\n\n" % (hashes, text)
227 |
228 | def convert_i(self, el, text):
229 | return self.convert_em(el, text)
230 |
231 | def convert_list(self, el, text):
232 | nested = False
233 | while el:
234 | if el.name == "li":
235 | nested = True
236 | break
237 | el = el.parent
238 | if nested:
239 | text = "\n" + self.indent(text, 1)
240 | return "\n" + text + "\n"
241 |
242 | convert_ul = convert_list
243 | convert_ol = convert_list
244 |
245 | def convert_li(self, el, text):
246 | parent = el.parent
247 | if parent is not None and parent.name == "ol":
248 | bullet = "%s." % (parent.index(el) + 1)
249 | else:
250 | depth = -1
251 | while el:
252 | if el.name == "ul":
253 | depth += 1
254 | el = el.parent
255 | bullets = self.options["bullets"]
256 | bullet = bullets[depth % len(bullets)]
257 | return "%s %s\n" % (bullet, text or "")
258 |
259 | def convert_p(self, el, text):
260 | return "%s\n\n" % text if text else ""
261 |
262 | def convert_strong(self, el, text):
263 | return "**%s**" % text if text else ""
264 |
265 | def convert_img(self, el, text):
266 | alt = el.attrs.get("alt", None) or ""
267 | src = el.attrs.get("src", None) or ""
268 | title = el.attrs.get("title", None) or ""
269 | title_part = ' "%s"' % title.replace('"', r"\"") if title else ""
270 | return "" % (alt, src, title_part)
271 |
272 |
273 | def html_to_markdown(html):
274 | html = html.replace("", "
---
")
275 | return MarkdownConverter(heading_style="atx").convert(html)
276 |
--------------------------------------------------------------------------------
/pydevto/markdownify_license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright 2012-2018 Matthew Tretter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/pydevto/pydevto.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 |
4 | class PyDevTo:
5 | def __init__(self, api_key=None, timeout=None):
6 | """
7 |
8 | :param api_key: Your dev.to api key (https://dev.to/settings/account)
9 | :param timeout: Timeout period for http requests
10 | """
11 | self.api_key = api_key
12 | self.timeout = timeout
13 |
14 | def public_articles(self, page=None, tag=None, username=None, state=None, top=None):
15 | """Return a list of public (published) articles
16 |
17 | :param page: pagination page
18 | :param tag: articles that contain the requested tag.
19 | :param username: articles belonging to a User or Organization ordered by descending published_at
20 | :param state: "fresh" or "rising". check which articles are fresh or rising.
21 | :param top: (int) most popular articles in the last N days
22 | :return:
23 | """
24 | return requests.get(
25 | "https://dev.to/api/articles",
26 | params={
27 | "page": page,
28 | "tag": tag,
29 | "username": username,
30 | "state": state,
31 | "top": top,
32 | },
33 | ).json()
34 |
35 | def public_article(self, id):
36 | """Return a single public (published) article given its id
37 |
38 | :param id: id of the article
39 | :return: article
40 | """
41 | return requests.get(
42 | "https://dev.to/api/articles/{id}".format(id=id), timeout=self.timeout
43 | ).json()
44 |
45 | def articles(self, page=None, per_page=None, state="published"):
46 | """Return a list of user articles
47 |
48 | :param page: pagination page
49 | :param per_page: page size
50 | :param state: "published", "unpublished" or "all
51 | :return: list of articles
52 | """
53 | url = "https://dev.to/api/articles/me"
54 | if state == "published":
55 | url = "https://dev.to/api/articles/me"
56 | elif state == "unpublished":
57 | url = "https://dev.to/api/articles/me/unpublished"
58 | elif state == "all":
59 | url = "https://dev.to/api/articles/me/all"
60 |
61 | return requests.get(
62 | url,
63 | params={"page": page, "per_page": per_page},
64 | headers={"api-key": self.api_key},
65 | timeout=self.timeout,
66 | ).json()
67 |
68 | def create_article(
69 | self,
70 | title,
71 | body_markdown="", # must default to empty string instead of None otherwise dev.to raises error on edit
72 | published=None,
73 | series=None,
74 | main_image=None,
75 | canonical_url=None,
76 | description=None,
77 | tags=None,
78 | organization_id=None,
79 | ):
80 | """Create an article
81 |
82 | :param title: Title
83 | :param body_markdown: Article Markdown content
84 | :param published: True to create published article, false otherwise
85 | :param series: Article series name
86 | :param main_image: Main image (or cover image)
87 | :param canonical_url: Canonical Url
88 | :param description: Article Description
89 | :param tags: List of article tags
90 | :param organization_id: Organization id
91 | :return: newly created article
92 | """
93 | url = "https://dev.to/api/articles"
94 |
95 | data = {
96 | "title": title,
97 | "body_markdown": body_markdown,
98 | "series": series,
99 | "published": published,
100 | "main_image": main_image,
101 | "canonical_url": canonical_url,
102 | "description": description,
103 | "tags": tags,
104 | "organization_id": organization_id,
105 | }
106 | # remove None keys from dict
107 | data = {k: v for k, v in data.items() if v is not None}
108 |
109 | return requests.post(
110 | url, json=data, headers={"api-key": self.api_key}, timeout=self.timeout
111 | ).json()
112 |
113 | def update_article(
114 | self,
115 | id,
116 | title=None,
117 | body_markdown=None,
118 | published=None,
119 | series=None,
120 | main_image=None,
121 | canonical_url=None,
122 | description=None,
123 | tags=None,
124 | organization_id=None,
125 | ):
126 | """Update an article
127 |
128 | :param id: id of article to update
129 | :param title: Title
130 | :param body_markdown: Article Markdown content
131 | :param published: True to create published article, false otherwise
132 | :param series: Article series name
133 | :param main_image: Main image (or cover image)
134 | :param canonical_url: Canonical Url
135 | :param description: Article Description
136 | :param tags: List of article tags
137 | :param organization_id: Organization id
138 | :return: updated article
139 | """
140 | url = "https://dev.to/api/articles/{id}".format(id=id)
141 |
142 | data = {
143 | "title": title,
144 | "body_markdown": body_markdown,
145 | "series": series,
146 | "published": published,
147 | "main_image": main_image,
148 | "canonical_url": canonical_url,
149 | "description": description,
150 | "tags": tags,
151 | "organization_id": organization_id,
152 | }
153 | # remove None keys from dict
154 | data = {k: v for k, v in data.items() if v is not None}
155 |
156 | return requests.put(
157 | url, json=data, headers={"api-key": self.api_key}, timeout=self.timeout
158 | ).json()
159 |
160 | def user(self, id=None, username=None):
161 | """Return user information
162 |
163 | If both id and username is None then information for user with the api_key (me) is returned
164 |
165 | :param id: (optional) id of user
166 | :param username: (optional) username of user
167 | :return: user object
168 | """
169 | url = "https://dev.to/api/users/me"
170 | if id:
171 | url = "https://dev.to/api/users/{id}".format(id=id)
172 | elif username:
173 | url = "https://dev.to/api/users/by_username"
174 |
175 | return requests.get(
176 | url,
177 | params={"url": username},
178 | headers={"api-key": self.api_key},
179 | timeout=self.timeout,
180 | ).json()
181 |
182 | def follow_suggestions(self, page=None):
183 | """Return list of follow suggestions
184 |
185 | :param page: pagination page
186 | :return: list of follow suggestions
187 | """
188 | return requests.get(
189 | "https://dev.to/api/users/?state=follow_suggestions",
190 | params={"page": page},
191 | headers={"api-key": self.api_key},
192 | timeout=self.timeout,
193 | ).json()
194 |
195 | def tags(self, page=None):
196 | """Return list of tags
197 |
198 | :param page: pagination page
199 | :return:
200 | """
201 | return requests.get(
202 | "https://dev.to/api/tags",
203 | params={"page": page},
204 | headers={"api-key": self.api_key},
205 | timeout=self.timeout,
206 | ).json()
207 |
208 | def webhooks(self):
209 | """Return list of webhooks
210 |
211 | :return: list of webhooks
212 | """
213 | return requests.get(
214 | "https://dev.to/api/webhooks",
215 | headers={"api-key": self.api_key},
216 | timeout=self.timeout,
217 | ).json()
218 |
219 | def webhook(self, id):
220 | """Return single webhook with id
221 |
222 | :param id: id of webhook
223 | :return: webhook object
224 | """
225 | return requests.get(
226 | "https://dev.to/api/webhooks/{id}".format(id=id),
227 | headers={"api-key": self.api_key},
228 | timeout=self.timeout,
229 | ).json()
230 |
231 | def create_webhook(self, source, target_url, events):
232 | """Create a new webhook
233 |
234 | :param source: The name of the requester, eg. "DEV"
235 | :param target_url: Target Url
236 | :param events: List of event identifiers
237 | :return:
238 | """
239 | return requests.post(
240 | "https://dev.to/api/webhooks",
241 | headers={"api-key": self.api_key},
242 | json={"source": source, "target_url": target_url, "events": events},
243 | timeout=self.timeout,
244 | ).json()
245 |
246 | def delete_webhook(self, id):
247 | """Delete a webhook with id
248 |
249 | :param id: id of webhook
250 | :return:
251 | """
252 | return requests.delete(
253 | "https://dev.to/api/webhooks/{id}".format(id=id),
254 | headers={"api-key": self.api_key},
255 | timeout=self.timeout,
256 | ).json()
257 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "pydevto"
3 | version = "0.1.4"
4 | description = "Unofficial dev.to api for python"
5 | readme = "README.md"
6 | repository = "https://github.com/lpellis/pydevto"
7 | license = "MIT"
8 | authors = ["'Loftie "]
9 |
10 | [tool.poetry.dependencies]
11 | python = "^3.6"
12 | beautifulsoup4 = "^4.8"
13 | six = "^1.12"
14 | requests = "^2.22"
15 |
16 | [tool.poetry.dev-dependencies]
17 | pytest = "^3.0"
18 |
19 | [build-system]
20 | requires = ["poetry>=0.12"]
21 | build-backend = "poetry.masonry.api"
22 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lpellis/pydevto/4c374759b6ae95846ee1648519809ac0e382cbbc/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_pydevto.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | import pydevto
4 | from pydevto import __version__
5 |
6 |
7 | def test_version():
8 | assert __version__ == "0.1.0"
9 |
10 |
11 | @pytest.mark.parametrize(
12 | "html,result",
13 | [
14 | ("
Observing Earth from space can alter an astronaut's perspective, a shift known as the “Overview Effect.” Described as a feeling of awe & responsibility for 🌎, get a taste of it yourself w/ these stunning accounts: https://t.co/d2kb7Ld4SWpic.twitter.com/4ukVsN2P3r