1 """Import logic for blueprint."""
3 from __future__
import annotations
5 from contextlib
import suppress
6 from dataclasses
import dataclass
10 import voluptuous
as vol
18 from .models
import Blueprint
19 from .schemas
import BLUEPRINT_SCHEMA, is_blueprint_config
21 COMMUNITY_TOPIC_PATTERN = re.compile(
22 r"^https://community.home-assistant.io/t/[a-z0-9-]+/(?P<topic>\d+)(?:/(?P<post>\d+)|)$"
25 COMMUNITY_CODE_BLOCK = re.compile(
26 r'<code class="lang-(?P<syntax>[a-z]+)">(?P<content>(?:.|\n)*)</code>', re.MULTILINE
29 GITHUB_FILE_PATTERN = re.compile(
30 r"^https://github.com/(?P<repository>.+)/blob/(?P<path>.+)$"
33 WEBSITE_PATTERN = re.compile(
34 r"^https://(?P<subdomain>[a-z0-9-]+)\.home-assistant\.io/(?P<path>.+).yaml$"
37 COMMUNITY_TOPIC_SCHEMA = vol.Schema(
41 "post_stream": {
"posts": [{
"updated_at": cv.datetime,
"cooked": str}]},
43 extra=vol.ALLOW_EXTRA,
48 """When the function doesn't support the url."""
51 @dataclass(frozen=
True)
53 """Imported blueprint."""
55 suggested_filename: str
61 """Convert a GitHub url to the raw content.
65 if url.startswith(
"https://raw.githubusercontent.com/"):
68 if (match := GITHUB_FILE_PATTERN.match(url))
is None:
71 repo, path = match.groups()
73 return f
"https://raw.githubusercontent.com/{repo}/{path}"
77 """Convert a forum post url to an import url.
81 if (match := COMMUNITY_TOPIC_PATTERN.match(url))
is None:
84 _topic, post = match.groups()
90 json_url = json_url[: -len(post) - 1]
100 ) -> ImportedBlueprint:
101 """Extract a blueprint from a community post JSON.
107 post = topic[
"post_stream"][
"posts"][0]
109 for match
in COMMUNITY_CODE_BLOCK.finditer(post[
"cooked"]):
110 block_syntax, block_content = match.groups()
112 if block_syntax
not in (
"auto",
"yaml"):
115 block_content = html.unescape(block_content.strip())
118 data = yaml.parse_yaml(block_content)
119 except HomeAssistantError:
120 if block_syntax ==
"yaml":
127 assert isinstance(data, dict)
129 blueprint =
Blueprint(data, schema=BLUEPRINT_SCHEMA)
132 if blueprint
is None:
134 "No valid blueprint found in the topic. Blueprint syntax blocks need to be"
135 " marked as YAML or no syntax."
139 f
'{post["username"]}/{topic["slug"]}', block_content, blueprint
144 hass: HomeAssistant, url: str
145 ) -> ImportedBlueprint:
146 """Get blueprints from a community post url.
148 Method can raise aiohttp client exceptions, vol.Invalid.
150 Caller needs to implement own timeout.
153 session = aiohttp_client.async_get_clientsession(hass)
155 resp = await session.get(import_url, raise_for_status=
True)
156 json_resp = await resp.json()
162 hass: HomeAssistant, url: str
163 ) -> ImportedBlueprint:
164 """Get a blueprint from a github url."""
166 session = aiohttp_client.async_get_clientsession(hass)
168 resp = await session.get(import_url, raise_for_status=
True)
169 raw_yaml = await resp.text()
170 data = yaml.parse_yaml(raw_yaml)
171 assert isinstance(data, dict)
172 blueprint =
Blueprint(data, schema=BLUEPRINT_SCHEMA)
174 parsed_import_url = yarl.URL(import_url)
175 suggested_filename = f
"{parsed_import_url.parts[1]}/{parsed_import_url.parts[-1]}"
176 if suggested_filename.endswith(
".yaml"):
177 suggested_filename = suggested_filename[:-5]
183 hass: HomeAssistant, url: str
184 ) -> ImportedBlueprint:
185 """Get a blueprint from a Github Gist."""
186 if not url.startswith(
"https://gist.github.com/"):
189 parsed_url = yarl.URL(url)
190 session = aiohttp_client.async_get_clientsession(hass)
192 resp = await session.get(
193 f
"https://api.github.com/gists/{parsed_url.parts[2]}",
194 headers={
"Accept":
"application/vnd.github.v3+json"},
195 raise_for_status=
True,
197 gist = await resp.json()
203 for filename, info
in gist[
"files"].items():
204 if not filename.endswith(
".yaml"):
207 content = info[
"content"]
208 data = yaml.parse_yaml(content)
212 assert isinstance(data, dict)
214 blueprint =
Blueprint(data, schema=BLUEPRINT_SCHEMA)
217 if blueprint
is None:
219 "No valid blueprint found in the gist. The blueprint file needs to end with"
224 f
"{gist['owner']['login']}/{filename[:-5]}", content, blueprint
229 hass: HomeAssistant, url: str
230 ) -> ImportedBlueprint:
231 """Get a blueprint from our website."""
232 if (WEBSITE_PATTERN.match(url))
is None:
235 session = aiohttp_client.async_get_clientsession(hass)
237 resp = await session.get(url, raise_for_status=
True)
238 raw_yaml = await resp.text()
239 data = yaml.parse_yaml(raw_yaml)
240 assert isinstance(data, dict)
241 blueprint =
Blueprint(data, schema=BLUEPRINT_SCHEMA)
243 parsed_import_url = yarl.URL(url)
244 suggested_filename = f
"homeassistant/{parsed_import_url.parts[-1][:-5]}"
249 hass: HomeAssistant, url: str
250 ) -> ImportedBlueprint:
251 """Get a blueprint from a generic website."""
252 session = aiohttp_client.async_get_clientsession(hass)
254 resp = await session.get(url, raise_for_status=
True)
255 raw_yaml = await resp.text()
256 data = yaml.parse_yaml(raw_yaml)
258 assert isinstance(data, dict)
259 blueprint =
Blueprint(data, schema=BLUEPRINT_SCHEMA)
261 parsed_import_url = yarl.URL(url)
262 suggested_filename = f
"{parsed_import_url.host}/{parsed_import_url.parts[-1][:-5]}"
267 fetch_blueprint_from_community_post,
268 fetch_blueprint_from_github_url,
269 fetch_blueprint_from_github_gist_url,
270 fetch_blueprint_from_website_url,
271 fetch_blueprint_from_generic_url,
276 """Get a blueprint from a url.
278 The returned blueprint will only be validated with BLUEPRINT_SCHEMA, not the domain
281 for func
in FETCH_FUNCTIONS:
282 with suppress(UnsupportedUrl):
283 imported_bp = await func(hass, url)
284 imported_bp.blueprint.update_metadata(source_url=url)
ImportedBlueprint fetch_blueprint_from_website_url(HomeAssistant hass, str url)
ImportedBlueprint fetch_blueprint_from_community_post(HomeAssistant hass, str url)
ImportedBlueprint fetch_blueprint_from_url(HomeAssistant hass, str url)
ImportedBlueprint _extract_blueprint_from_community_topic(str url, dict topic)
str _get_community_post_import_url(str url)
str _get_github_import_url(str url)
ImportedBlueprint fetch_blueprint_from_github_url(HomeAssistant hass, str url)
ImportedBlueprint fetch_blueprint_from_github_gist_url(HomeAssistant hass, str url)
ImportedBlueprint fetch_blueprint_from_generic_url(HomeAssistant hass, str url)
bool is_blueprint_config(Any config)