Skip to content

Commit ae9de23

Browse files
authored
Implement support for IPFS/IPNS native URL (#17)
* Implement support for IPFS/IPNS native URL * Sort imports
1 parent 1212c2c commit ae9de23

File tree

9 files changed

+127
-11
lines changed

9 files changed

+127
-11
lines changed

README.MD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ print(Validator("QmYjtig7VJQ6XsnUjqqJvj7QaMcCAwtrgNdahSiFofrE7o").is_ipfs())
3131
* [x] Path
3232
* [x] IPFS
3333
* [x] IPNS
34+
* [x] Native URL
35+
* [x] IPFS
36+
* [x] IPNS
3437
* [ ] ...
3538

3639
## License

is_ipfs/is_ipfs.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ def __init__(self, input: typing.Any):
2121
r"^https?://(?P<hash>[^/]+)\.(?P<protocol>ip[fn]s)\.[^/?]+"
2222
)
2323
self.path_pattern = re.compile(r"^/(?P<protocol>ip[fn]s)/(?P<hash>[^/?#]+)")
24+
self.native_url_pattern = re.compile(
25+
r"^(?P<protocol>ip[fn]s)://(?P<hash>[^/?#]+)"
26+
)
2427

2528
def is_ipfs(self) -> bool:
2629
"""
@@ -32,6 +35,8 @@ def is_ipfs(self) -> bool:
3235
or self._is_ipns_url()
3336
or self._is_ipfs_path()
3437
or self._is_ipns_path()
38+
or self._is_native_ipfs_url()
39+
or self._is_native_ipns_url()
3540
)
3641

3742
def _is_cid(self) -> bool:
@@ -75,7 +80,10 @@ def _is_integral_ipfs_url(
7580

7681
_hash = match["hash"]
7782

78-
if pattern == self.subdomain_gateway_pattern:
83+
if (
84+
pattern == self.subdomain_gateway_pattern
85+
or pattern == self.native_url_pattern
86+
):
7987
_hash = _hash.lower()
8088
try:
8189
if get_codec(_hash).encoding not in ["base32", "base36"]:
@@ -152,7 +160,10 @@ def _is_integral_ipns_url(
152160

153161
ipns_id = match["hash"]
154162

155-
if ipns_id and pattern == self.subdomain_gateway_pattern:
163+
if (ipns_id) and (
164+
pattern == self.subdomain_gateway_pattern
165+
or pattern == self.native_url_pattern
166+
):
156167
ipns_id = ipns_id.lower()
157168

158169
if Validator(ipns_id)._is_cid():
@@ -163,7 +174,9 @@ def _is_integral_ipns_url(
163174
print(f"Unexpected {type(error)}, {error}")
164175
return False
165176
try:
166-
if "." not in ipns_id and "-" in ipns_id:
177+
if ("." not in ipns_id and "-" in ipns_id) and (
178+
pattern == self.subdomain_gateway_pattern
179+
):
167180
ipns_id = (
168181
ipns_id.replace("--", "@").replace("-", ".").replace("@", "-")
169182
)
@@ -234,3 +247,19 @@ def _id_is_explicit_tld(self, input_string: str) -> bool:
234247
)
235248
hostname = urlparse(f"http://{input_string}").hostname
236249
return bool(re.search(fqdn_with_tld, hostname))
250+
251+
def _is_native_ipfs_url(self) -> bool:
252+
"""
253+
Returns True if the provided string is a valid IPFS native URL or False otherwise.
254+
"""
255+
return self._is_integral_ipfs_url(
256+
self.native_url_pattern,
257+
)
258+
259+
def _is_native_ipns_url(self) -> bool:
260+
"""
261+
Returns True if the provided string is a valid IPNS native URL or False otherwise.
262+
"""
263+
return self._is_integral_ipns_url(
264+
self.native_url_pattern,
265+
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
setuptools.setup(
88
name="py-is_ipfs",
9-
version="0.1.0",
9+
version="0.2.0",
1010
description="Python library to identify valid IPFS resources",
1111
long_description=long_description,
1212
long_description_content_type="text/markdown",

tests/integration/test_is_ipfs.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import unittest
22

3-
import tests.testing_data as testing_data
43
from is_ipfs import Validator
4+
from tests import testing_data
55

66

77
class TestCase(unittest.TestCase):
@@ -65,6 +65,22 @@ def test_all(self):
6565
for entry in testing_data.invalid_entries["path"]["ipns"]:
6666
self.assertFalse(Validator(entry).is_ipfs())
6767

68+
with self.subTest("Test valid IPFS native URL entries from fixtures"):
69+
for entry in testing_data.valid_entries["native_url"]["ipfs"]:
70+
self.assertTrue(Validator(entry).is_ipfs())
71+
72+
with self.subTest("Test invalid IPFS native URL entries from fixtures"):
73+
for entry in testing_data.invalid_entries["native_url"]["ipfs"]:
74+
self.assertFalse(Validator(entry).is_ipfs())
75+
76+
with self.subTest("Test valid IPNS native URL entries from fixtures"):
77+
for entry in testing_data.valid_entries["native_url"]["ipns"]:
78+
self.assertTrue(Validator(entry).is_ipfs())
79+
80+
with self.subTest("Test invalid IPNS native URL from fixtures"):
81+
for entry in testing_data.invalid_entries["native_url"]["ipns"]:
82+
self.assertFalse(Validator(entry).is_ipfs())
83+
6884

69-
if __name__ == "__main__":
85+
if __name__ == "__main__": # pragma: no cover
7086
unittest.main()

tests/testing_data.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,25 @@
125125
"/ipns/uAXIAJAgBEiB6C43WeEQO-iBUnhM_SS2I_pHzemJnV7m_TKgqgiiJoQ", # base64url
126126
],
127127
},
128+
"native_url": {
129+
"ipfs": [
130+
"ipfs://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va", # base32
131+
"ipfs://bafybeidvtwx54qr44kidymvhfzefzxhgkieigwth6oswk75zhlzjdmunoy/linkify-demo.html", # base32
132+
"ipfs://bafybeie5gq4jxvzmsym6hjlwxej4rwdoxt7wadqvmmwbqi7r27fclha2va", # base32
133+
"ipfs://bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/", # base32
134+
"ipfs://k2jmtxw8rjh1z69c6not3wtdxb0u3urbzhyll1t9jg6ox26dhi5sfi1m/", # base36
135+
],
136+
"ipns": [
137+
"ipns://docs.ipfs.io.ipns.localhost:8080/some/path",
138+
"ipns://docs.ipfs.io.ipns.foo.bar.buzz.dweb.link",
139+
"ipns://docs.ipfs.io.ipns.locahost:8080",
140+
"ipns://en-wikipedia--on--ipfs-org.ipns.dweb.link",
141+
"ipns://cid.ipfs.io",
142+
"ipns://ipfs-io.ipns.dweb.link/",
143+
"ipns://k51qzi5uqu5dj83g1ajm0a9z06fny53f7cb5co9k6pvo8b0yr871vinhrohbq9", # base36
144+
"ipns://K51QZI5UQU5DJ83G1AJM0A9Z06FNY53F7CB5CO9K6PVO8B0YR871VINHROHBQ9/path#title", # base36upper
145+
],
146+
},
128147
}
129148

130149
invalid_entries = {
@@ -208,4 +227,24 @@
208227
"/ipns/mAXASIMPEcz7Ir/0Gz56f9Q/8a80uyFphcABLtwlmnDHelDka",
209228
],
210229
},
230+
"native_url": {
231+
"ipfs": [
232+
"ipfs://js-ipfs/blob/master/README.md",
233+
"ipfs://QmcNioXSC1bfJj1dcFErhUfyjFzoX2HodkRccsFFVJJvg8.ipns.dweb.link",
234+
"ipfs://QmeWz9YZEeNFXQhHg4PnR5ZiNr5isttgi5n1tc1eD5EfGU"
235+
"ipfs://foo/QmeWz9YZEeNFXQhHg4PnR5ZiNr5isttgi5n1tc1eD5EfGU",
236+
"foo://bar",
237+
"ipfs:/mAXASIMPEcz7Ir/0Gz56f9Q/8a80uyFphcABLtwlmnDHelDka",
238+
"ipfs://1234",
239+
],
240+
"ipns": [
241+
"ipns://bafzaajaiaejca6qlrxlhqrao7iqfjhqth5es3ch6shzxuythk6436tfifkbcrcnb",
242+
"ipns://qmbwqxbekC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR",
243+
"ipfs://js-ipfs/blob/master/README.md",
244+
"ipns://en-wikipedia--on--ipfs-org/",
245+
"foo://bar",
246+
"ipns:///1234",
247+
"ipns:///mAXASIMPEcz7Ir/0Gz56f9Q/8a80uyFphcABLtwlmnDHelDka",
248+
],
249+
},
211250
}

tests/unit/test_native_url.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
3+
from is_ipfs import Validator
4+
from tests import testing_data
5+
6+
7+
class TestCase(unittest.TestCase):
8+
def test_native_url(self):
9+
with self.subTest("Test valid IPFS native URL entries from fixtures"):
10+
for entry in testing_data.valid_entries["native_url"]["ipfs"]:
11+
self.assertTrue(Validator(entry)._is_native_ipfs_url())
12+
13+
with self.subTest("Test invalid IPFS native URL entries from fixtures"):
14+
for entry in testing_data.invalid_entries["native_url"]["ipfs"]:
15+
self.assertFalse(Validator(entry)._is_native_ipfs_url())
16+
17+
with self.subTest("Test valid IPNS native URL entries from fixtures"):
18+
for entry in testing_data.valid_entries["native_url"]["ipns"]:
19+
self.assertTrue(Validator(entry)._is_native_ipns_url())
20+
21+
with self.subTest("Test invalid IPNS native URL from fixtures"):
22+
for entry in testing_data.invalid_entries["native_url"]["ipns"]:
23+
self.assertFalse(Validator(entry)._is_native_ipns_url())
24+
25+
26+
if __name__ == "__main__": # pragma: no cover
27+
unittest.main()

tests/unit/test_path.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import unittest
22

3-
import tests.testing_data as testing_data
43
from is_ipfs import Validator
4+
from tests import testing_data
55

66

77
class TestCase(unittest.TestCase):

tests/unit/test_subdomain.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from is_ipfs import Validator
21
import unittest
3-
import tests.testing_data as testing_data
2+
3+
from is_ipfs import Validator
4+
from tests import testing_data
45

56

67
class TestCase(unittest.TestCase):

tests/unit/test_url.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
from is_ipfs import Validator
21
import unittest
3-
import tests.testing_data as testing_data
2+
3+
from is_ipfs import Validator
4+
from tests import testing_data
45

56

67
class TestCase(unittest.TestCase):

0 commit comments

Comments
 (0)