Coverage for webapp/integrations/logic.py: 21%
145 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-18 22:11 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-18 22:11 +0000
1import re
2from github import Github
3from os import getenv
5from redis_cache.cache_utility import redis_cache
6from webapp.helpers import get_yaml_loader
7from webapp.observability.utils import trace_function
8from webapp.packages.logic import (
9 get_package,
10)
12GITHUB_TOKEN = getenv("GITHUB_TOKEN")
14github_client = Github(GITHUB_TOKEN)
15yaml = get_yaml_loader()
18class Interfaces:
19 def __init__(self):
20 self._repo = None
22 @property
23 def repo(self):
24 if self._repo is None:
25 self._repo = github_client.get_repo(
26 "canonical/charmlibs"
27 )
28 return self._repo
30 @trace_function
31 def get_interfaces(self):
32 key = "get-interfaces"
33 interfaces = redis_cache.get(key, expected_type=list)
34 if interfaces:
35 return interfaces
36 try:
37 index = self.repo.get_contents("interfaces/index.json")
38 interfaces = yaml.load(index.decoded_content.decode("utf-8"))
40 normalized_interfaces = []
41 for interface in interfaces or []:
42 normalized_interfaces.append(
43 {
44 "name": interface.get("name", ""),
45 "status": interface.get("status", ""),
46 "summary": interface.get("summary", ""),
47 "description": interface.get("description", ""),
48 "version": interface.get("version", ""),
49 "tags": interface.get("tags", []),
50 "links": {
51 "library": interface.get("lib_url"),
52 "documentation": interface.get("docs_url")
53 },
54 }
55 )
57 redis_cache.set(key, normalized_interfaces, ttl=86400)
58 return normalized_interfaces
59 except Exception:
60 return []
62 @trace_function
63 def get_interface_from_path(self, interface_name):
64 key = f"get-interface-from-path:{interface_name}"
65 interface = redis_cache.get(key, expected_type=dict)
66 if interface:
67 return interface
68 try:
69 interface_versions = self.repo.get_contents(
70 "interfaces/{}/interface".format(interface_name)
71 )
72 except Exception:
73 return None
74 versions = []
75 if not isinstance(interface_versions, list):
76 interface_versions = [interface_versions]
77 for _, version in enumerate(interface_versions):
78 if version.type == "dir" and version.name.startswith("v"):
79 versions.append(version.name)
81 versions.sort()
83 latest_version = versions.pop()
85 latest_version_interface = self.repo.get_contents(
86 "interfaces/{}/interface/{}/interface.yaml".format(
87 interface_name, latest_version
88 )
89 ).decoded_content.decode("utf-8")
90 interface = yaml.load(latest_version_interface)
92 active_providers = []
93 active_requirers = []
95 if "providers" in interface and interface["providers"]:
96 active_providers = []
97 for provider in interface["providers"]:
98 try:
99 get_package(provider["name"], [], False)
100 active_providers.append(provider)
101 except Exception:
102 continue
103 interface["providers"] = active_providers
105 if "requirers" in interface and interface["requirers"]:
106 active_requirers = []
107 for requirer in interface["requirers"]:
108 try:
109 get_package(requirer["name"], [], False)
110 active_requirers.append(requirer)
111 except Exception:
112 continue
113 interface["requirers"] = active_requirers
114 redis_cache.set(key, interface, ttl=86400)
115 return interface
117 @trace_function
118 def get_h_content(self, text, pattern):
119 start_index = text.index(pattern)
120 return [start_index, start_index + len(pattern)]
122 @trace_function
123 def extract_headings_and_content(self, text, level):
124 headings = re.findall(
125 r"^#{" + str(level) + r"}\s.*", text, flags=re.MULTILINE
126 )
128 start_end = {
129 heading: self.get_h_content(text, heading) for heading in headings
130 }
131 result = []
132 for i in range(len(headings)):
133 current_heading = headings[i]
134 start_index = start_end[current_heading][1]
135 has_next = i < len(headings) - 1
136 if has_next:
137 next_heading = headings[i + 1]
138 end_index = start_end[next_heading][0]
139 body = text[start_index:end_index]
140 else:
141 body = text[start_index:]
143 result.append([current_heading.strip(), body.strip()])
144 return result
146 @trace_function
147 def parse_text(self, interface, version, text):
148 base_link = (
149 "https://github.com/canonical/"
150 "charmlibs/blob/main/interfaces/{}/interface/v{}"
151 ).format(interface, version)
152 pattern = r"\[.*?\]\(.*?\)"
153 matches = re.findall(pattern, text)
155 for match in matches:
156 element_pattern = r"\[(.*?)\]\((.*?)\)"
157 element_match = re.search(element_pattern, match)
158 if element_match:
159 title = element_match.group(1)
160 url = element_match.group(2)
161 absolute_url = url
162 if absolute_url.startswith("./"):
163 absolute_url = absolute_url.replace("./", base_link + "/")
165 text = text.replace(match, f"[{title}]({absolute_url})")
167 return text
169 @trace_function
170 def convert_readme(self, interface, version, text, level=2):
171 headings_and_contents = self.extract_headings_and_content(text, level)
173 if len(headings_and_contents) == 0:
174 return [s.strip("\n") for s in text.split("\n") if s.strip("\n")]
176 resulting_list = []
178 for heading, content in headings_and_contents:
179 strip_char = "{}{}".format("#" * level, " ")
180 heading = heading.strip(strip_char)
181 _temp = {"heading": heading, "level": level, "children": []}
183 children = []
185 result = self.convert_readme(
186 interface, version, content, level + 1
187 )
189 if len(content) > 0:
190 body = content.split("#")[0].strip()
191 body_list = body.split("\n\n")
192 if len(list(filter(None, body_list))) > 0:
193 children += body_list
195 if isinstance(result, list) and isinstance(result[0], dict):
196 children += result
198 for child in children:
199 if isinstance(child, list):
200 _temp["children"] += child
201 else:
202 _temp["children"].append(child)
204 for index, child in list(enumerate(_temp["children"])):
205 if isinstance(child, str):
206 _temp["children"][index] = self.parse_text(
207 interface, version, child
208 )
210 resulting_list.append(_temp)
212 return resulting_list
214 @trace_function
215 def get_interface_name_from_readme(self, text):
216 name = re.sub(r"[#` \n]", "", text.split("\n##", 1)[0]).split("/")[0]
217 return name
220interface_logic = Interfaces()