Skip to content

Commit 664ecf1

Browse files
Charm URL type
The following introduces a charm URL, this will help when trying to ensure that what we get from the API is indeed a valid URL. The code is pretty much a lift from the juju/charm package, minus the FQDN charm URLs, which should never have been added to the charm package. The code is rather procedural and doesn't validate the name or series, but the controller can do that once we know the pattern is correct. Essentially we want the charm URL for the schema and the name, so rather than have methods that sort of do the job, we should replicate the exact same parsing layout so we don't get it wrong.
1 parent 195191b commit 664ecf1

2 files changed

Lines changed: 179 additions & 0 deletions

File tree

juju/url.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from enum import Enum
2+
from .errors import JujuError
3+
from urllib.parse import urlparse
4+
5+
6+
class Schema(Enum):
7+
LOCAL = "local"
8+
CHARM_STORE = "cs"
9+
CHARM_HUB = "ch"
10+
11+
def matches(self, potential):
12+
return self.value == potential
13+
14+
def __str__(self):
15+
return self.value
16+
17+
18+
class URL:
19+
def __init__(self, schema, user=None, name=None, revision=None, series=None):
20+
self.schema = schema
21+
self.user = user
22+
self.name = name
23+
self.series = series
24+
25+
# 0 can be a valid revision, hence the more verbose check.
26+
if revision is None:
27+
revision = -1
28+
self.revision = revision
29+
30+
@staticmethod
31+
def parse(s):
32+
"""parse parses the provided charm URL string into its respective
33+
structure.
34+
35+
A missing schema is assumed to be 'ch'.
36+
37+
"""
38+
u = urlparse(s)
39+
if u.query != "" or u.fragment != "" or u.username or u.password:
40+
raise JujuError("charm or bundle URL {} has unrecognized parts".format(u))
41+
42+
if Schema.LOCAL.matches(u.scheme):
43+
c = parse_v1_url(Schema.LOCAL, u, s)
44+
elif Schema.CHARM_STORE.matches(u.scheme):
45+
c = parse_v1_url(Schema.CHARM_STORE, u, s)
46+
else:
47+
c = parse_v2_url(u, s)
48+
49+
if not c.schema:
50+
raise JujuError("expected schema for charm or bundle URL {}".format(u))
51+
return c
52+
53+
def with_revision(self, rev):
54+
return URL(self.schema, self.user, self.name, rev, self.series)
55+
56+
def path(self):
57+
parts = []
58+
if self.user:
59+
parts.append("~{}".format(self.user))
60+
if self.series:
61+
parts.append(self.series)
62+
if self.revision is not None and self.revision >= 0:
63+
parts.append("{}-{}", self.name, self.revision)
64+
else:
65+
parts.append(self.name)
66+
return "/".join(parts)
67+
68+
def __eq__(self, other):
69+
if isinstance(other, self.__class__):
70+
return self.schema == other.schema and \
71+
self.user == other.user and \
72+
self.name == other.name and \
73+
self.revision == other.revision and \
74+
self.series == other.series
75+
return False
76+
77+
def __str__(self):
78+
return "{}:{}".format(str(self.schema), self.path())
79+
80+
81+
def parse_v1_url(schema, u, s):
82+
c = URL(schema)
83+
84+
parts = u.path.split("/")
85+
if len(parts) < 1 or len(parts) > 4:
86+
raise JujuError("charm or bundle URL has invalid form {}".format(s))
87+
88+
# ~<username>
89+
if parts[0].startswith("~"):
90+
if schema == Schema.LOCAL:
91+
raise JujuError("local charm or bundle URL with username {}".format(s))
92+
c.user = parts[0][1:]
93+
parts = parts[1:]
94+
95+
if len(parts) > 2:
96+
raise JujuError("charm or bundle URL has invalid form {}".format(s))
97+
98+
# <series>
99+
if len(parts) == 2:
100+
c.series = parts[0]
101+
parts = parts[1:]
102+
# TODO (stickupkid) - validate the series.
103+
104+
if len(parts) < 1:
105+
raise JujuError("URL without charm or bundle name {}".format(s))
106+
107+
(c.name, c.revision) = extract_revision(parts[0])
108+
# TODO (stickupkid) - validate the name.
109+
110+
return c
111+
112+
113+
def parse_v2_url(u, s):
114+
c = URL(Schema.CHARM_HUB)
115+
116+
parts = u.path.split("/")
117+
if len(parts) != 1:
118+
raise JujuError("charm or bundle URL {} malformed, expected <name>".format(s))
119+
120+
(c.name, c.revision) = extract_revision(parts[0])
121+
# TODO (stickupkid) - validate the name.
122+
123+
return c
124+
125+
126+
def extract_revision(name):
127+
revision = -1
128+
for i in range(len(name) - 1, -1, -1):
129+
c = name[i]
130+
if c.isnumeric():
131+
continue
132+
if c == "-" and i != (len(name) - 1):
133+
revision = int(name[(i + 1):])
134+
name = name[:i]
135+
break
136+
return (name, revision)

tests/unit/test_url.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import unittest
2+
3+
from juju.url import Schema, URL
4+
5+
6+
class TestURLV1(unittest.TestCase):
7+
def test_parse_charmstore(self):
8+
u = URL.parse("cs:mysql")
9+
self.assertEqual(u, URL(Schema.CHARM_STORE, name="mysql"))
10+
11+
def test_parse_local(self):
12+
u = URL.parse("local:mysql")
13+
self.assertEqual(u, URL(Schema.LOCAL, name="mysql"))
14+
15+
def test_parse_v1_user(self):
16+
u = URL.parse("cs:~fred/mysql")
17+
self.assertEqual(u, URL(Schema.CHARM_STORE, name="mysql", user="fred"))
18+
19+
def test_parse_v1_revision(self):
20+
u = URL.parse("cs:~fred/mysql-1")
21+
self.assertEqual(u, URL(Schema.CHARM_STORE, name="mysql", user="fred", revision=1))
22+
23+
def test_parse_v1_large_revision(self):
24+
u = URL.parse("cs:~fred/mysql-12345")
25+
self.assertEqual(u, URL(Schema.CHARM_STORE, name="mysql", user="fred", revision=12345))
26+
27+
def test_parse_v1_series(self):
28+
u = URL.parse("cs:~fred/bionic/mysql-1")
29+
self.assertEqual(u, URL(Schema.CHARM_STORE, name="mysql", user="fred", revision=1, series="bionic"))
30+
31+
32+
class TestURLV2(unittest.TestCase):
33+
def test_parse_charmhub(self):
34+
u = URL.parse("ch:mysql")
35+
self.assertEqual(u, URL(Schema.CHARM_HUB, name="mysql"))
36+
37+
def test_parse_v2_revision(self):
38+
u = URL.parse("ch:mysql-1")
39+
self.assertEqual(u, URL(Schema.CHARM_HUB, name="mysql", revision=1))
40+
41+
def test_parse_v2_large_revision(self):
42+
u = URL.parse("ch:mysql-12345")
43+
self.assertEqual(u, URL(Schema.CHARM_HUB, name="mysql", revision=12345))

0 commit comments

Comments
 (0)