blob: b565872bddea24d2f22999041b1714db18ca4ba5 [file] [log] [blame]
Joe Gregorio79daca02013-03-29 16:25:52 -04001#!/usr/bin/python
Joe Gregorio20a5aa92011-04-01 17:44:25 -04002#
Craig Citro751b7fb2014-09-23 11:20:38 -07003# Copyright 2014 Google Inc. All Rights Reserved.
Joe Gregorio20a5aa92011-04-01 17:44:25 -04004#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
Joe Gregorio81d92cc2012-07-09 16:46:02 -040017"""Create documentation for generate API surfaces.
18
19Command-line tool that creates documentation for all APIs listed in discovery.
20The documentation is generated from a combination of the discovery document and
21the generated API surface itself.
22"""
Christian Clauss9fdc2b22019-07-22 19:43:21 +020023from __future__ import print_function
Joe Gregorio81d92cc2012-07-09 16:46:02 -040024
Bu Sun Kim66bb32c2019-10-30 10:11:58 -070025__author__ = "jcgregorio@google.com (Joe Gregorio)"
Joe Gregorio20a5aa92011-04-01 17:44:25 -040026
Bu Sun Kimc9773042019-07-17 14:03:17 -070027from collections import OrderedDict
Joe Gregorio79daca02013-03-29 16:25:52 -040028import argparse
Bu Sun Kimc9773042019-07-17 14:03:17 -070029import collections
Craig Citro6ae34d72014-08-18 23:10:09 -070030import json
Anthonios Partheniouaff037a2021-04-21 11:00:09 -040031import pathlib
Joe Gregorioafc45f22011-02-20 16:11:28 -050032import re
Joe Gregorio79daca02013-03-29 16:25:52 -040033import string
Joe Gregorio20a5aa92011-04-01 17:44:25 -040034import sys
Joe Gregorioafc45f22011-02-20 16:11:28 -050035
John Asmuth864311d2014-04-24 15:46:08 -040036from googleapiclient.discovery import DISCOVERY_URI
37from googleapiclient.discovery import build
38from googleapiclient.discovery import build_from_document
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -080039from googleapiclient.discovery import UnknownApiNameOrVersion
Igor Maravić22435292017-01-19 22:28:22 +010040from googleapiclient.http import build_http
Dan O'Mearadd494642020-05-01 07:42:23 -070041from googleapiclient.errors import HttpError
42
Joe Gregorio81d92cc2012-07-09 16:46:02 -040043import uritemplate
44
Anthonios Partheniouaff037a2021-04-21 11:00:09 -040045DISCOVERY_DOC_DIR = (
46 pathlib.Path(__file__).parent.resolve() / "googleapiclient" / "discovery_cache" / "documents"
47)
48
Joe Gregorio81d92cc2012-07-09 16:46:02 -040049CSS = """<style>
50
51body, h1, h2, h3, div, span, p, pre, a {
52 margin: 0;
53 padding: 0;
54 border: 0;
55 font-weight: inherit;
56 font-style: inherit;
57 font-size: 100%;
58 font-family: inherit;
59 vertical-align: baseline;
60}
61
62body {
63 font-size: 13px;
64 padding: 1em;
65}
66
67h1 {
68 font-size: 26px;
69 margin-bottom: 1em;
70}
71
72h2 {
73 font-size: 24px;
74 margin-bottom: 1em;
75}
76
77h3 {
78 font-size: 20px;
79 margin-bottom: 1em;
80 margin-top: 1em;
81}
82
83pre, code {
84 line-height: 1.5;
85 font-family: Monaco, 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Lucida Console', monospace;
86}
87
88pre {
89 margin-top: 0.5em;
90}
91
92h1, h2, h3, p {
93 font-family: Arial, sans serif;
94}
95
96h1, h2, h3 {
97 border-bottom: solid #CCC 1px;
98}
99
100.toc_element {
101 margin-top: 0.5em;
102}
103
104.firstline {
105 margin-left: 2 em;
106}
107
108.method {
109 margin-top: 1em;
110 border: solid 1px #CCC;
111 padding: 1em;
112 background: #EEE;
113}
114
115.details {
116 font-weight: bold;
117 font-size: 14px;
118}
119
120</style>
121"""
122
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400123METHOD_TEMPLATE = """<div class="method">
124 <code class="details" id="$name">$name($params)</code>
125 <pre>$doc</pre>
126</div>
127"""
128
129COLLECTION_LINK = """<p class="toc_element">
130 <code><a href="$href">$name()</a></code>
131</p>
132<p class="firstline">Returns the $name Resource.</p>
133"""
134
135METHOD_LINK = """<p class="toc_element">
136 <code><a href="#$name">$name($params)</a></code></p>
137<p class="firstline">$firstline</p>"""
138
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400139BASE = pathlib.Path(__file__).parent.resolve() / "docs" / "dyn"
Joe Gregoriobb964352013-03-03 20:45:29 -0500140
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700141DIRECTORY_URI = "https://www.googleapis.com/discovery/v1/apis"
Joe Gregoriobb964352013-03-03 20:45:29 -0500142
Joe Gregorio79daca02013-03-29 16:25:52 -0400143parser = argparse.ArgumentParser(description=__doc__)
Joe Gregoriobb964352013-03-03 20:45:29 -0500144
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700145parser.add_argument(
146 "--discovery_uri_template",
147 default=DISCOVERY_URI,
148 help="URI Template for discovery.",
149)
Joe Gregoriobb964352013-03-03 20:45:29 -0500150
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700151parser.add_argument(
152 "--discovery_uri",
153 default="",
154 help=(
155 "URI of discovery document. If supplied then only "
156 "this API will be documented."
157 ),
158)
Joe Gregoriobb964352013-03-03 20:45:29 -0500159
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700160parser.add_argument(
161 "--directory_uri",
162 default=DIRECTORY_URI,
163 help=("URI of directory document. Unused if --discovery_uri" " is supplied."),
164)
Joe Gregoriobb964352013-03-03 20:45:29 -0500165
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700166parser.add_argument(
167 "--dest", default=BASE, help="Directory name to write documents into."
168)
Joe Gregoriobb964352013-03-03 20:45:29 -0500169
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400170
171def safe_version(version):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700172 """Create a safe version of the verion string.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400173
174 Needed so that we can distinguish between versions
175 and sub-collections in URIs. I.e. we don't want
176 adsense_v1.1 to refer to the '1' collection in the v1
177 version of the adsense api.
178
179 Args:
180 version: string, The version string.
181 Returns:
182 The string with '.' replaced with '_'.
183 """
184
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700185 return version.replace(".", "_")
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400186
187
188def unsafe_version(version):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700189 """Undoes what safe_version() does.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400190
191 See safe_version() for the details.
192
193
194 Args:
195 version: string, The safe version string.
196 Returns:
197 The string with '_' replaced with '.'.
198 """
199
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700200 return version.replace("_", ".")
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400201
202
203def method_params(doc):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700204 """Document the parameters of a method.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400205
206 Args:
207 doc: string, The method's docstring.
208
209 Returns:
210 The method signature as a string.
211 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700212 doclines = doc.splitlines()
213 if "Args:" in doclines:
214 begin = doclines.index("Args:")
215 if "Returns:" in doclines[begin + 1 :]:
216 end = doclines.index("Returns:", begin)
217 args = doclines[begin + 1 : end]
218 else:
219 args = doclines[begin + 1 :]
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400220
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700221 parameters = []
Anthonios Partheniou4249a7b2020-12-15 20:32:05 -0500222 sorted_parameters = []
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700223 pname = None
224 desc = ""
225
226 def add_param(pname, desc):
227 if pname is None:
228 return
229 if "(required)" not in desc:
230 pname = pname + "=None"
Anthonios Partheniou4249a7b2020-12-15 20:32:05 -0500231 parameters.append(pname)
232 else:
233 # required params should be put straight into sorted_parameters
234 # to maintain order for positional args
235 sorted_parameters.append(pname)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700236
237 for line in args:
Karthikeyan Singaravelan0f60eda2020-08-03 22:12:03 +0530238 m = re.search(r"^\s+([a-zA-Z0-9_]+): (.*)", line)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700239 if m is None:
240 desc += line
241 continue
242 add_param(pname, desc)
243 pname = m.group(1)
244 desc = m.group(2)
245 add_param(pname, desc)
Anthonios Partheniou4249a7b2020-12-15 20:32:05 -0500246 sorted_parameters.extend(sorted(parameters))
247 sorted_parameters = ", ".join(sorted_parameters)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700248 else:
Anthonios Partheniou4249a7b2020-12-15 20:32:05 -0500249 sorted_parameters = ""
250 return sorted_parameters
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400251
252
253def method(name, doc):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700254 """Documents an individual method.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400255
256 Args:
257 name: string, Name of the method.
258 doc: string, The methods docstring.
259 """
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400260 import html
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400261
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700262 params = method_params(doc)
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400263 doc = html.escape(doc)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700264 return string.Template(METHOD_TEMPLATE).substitute(
265 name=name, params=params, doc=doc
266 )
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400267
268
269def breadcrumbs(path, root_discovery):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700270 """Create the breadcrumb trail to this page of documentation.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400271
272 Args:
273 path: string, Dot separated name of the resource.
274 root_discovery: Deserialized discovery document.
275
276 Returns:
277 HTML with links to each of the parent resources of this resource.
278 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700279 parts = path.split(".")
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400280
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700281 crumbs = []
282 accumulated = []
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400283
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700284 for i, p in enumerate(parts):
285 prefix = ".".join(accumulated)
286 # The first time through prefix will be [], so we avoid adding in a
287 # superfluous '.' to prefix.
288 if prefix:
289 prefix += "."
290 display = p
291 if i == 0:
292 display = root_discovery.get("title", display)
Karthikeyan Singaravelan0f60eda2020-08-03 22:12:03 +0530293 crumbs.append('<a href="{}.html">{}</a>'.format(prefix + p, display))
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700294 accumulated.append(p)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400295
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700296 return " . ".join(crumbs)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400297
298
299def document_collection(resource, path, root_discovery, discovery, css=CSS):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700300 """Document a single collection in an API.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400301
302 Args:
303 resource: Collection or service being documented.
304 path: string, Dot separated name of the resource.
305 root_discovery: Deserialized discovery document.
306 discovery: Deserialized discovery document, but just the portion that
307 describes the resource.
308 css: string, The CSS to include in the generated file.
309 """
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700310 collections = []
311 methods = []
312 resource_name = path.split(".")[-2]
313 html = [
314 "<html><body>",
315 css,
316 "<h1>%s</h1>" % breadcrumbs(path[:-1], root_discovery),
317 "<h2>Instance Methods</h2>",
318 ]
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400319
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700320 # Which methods are for collections.
321 for name in dir(resource):
322 if not name.startswith("_") and callable(getattr(resource, name)):
323 if hasattr(getattr(resource, name), "__is_resource__"):
324 collections.append(name)
325 else:
326 methods.append(name)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500327
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700328 # TOC
329 if collections:
330 for name in collections:
331 if not name.startswith("_") and callable(getattr(resource, name)):
332 href = path + name + ".html"
333 html.append(
334 string.Template(COLLECTION_LINK).substitute(href=href, name=name)
335 )
Joe Gregorioafc45f22011-02-20 16:11:28 -0500336
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700337 if methods:
338 for name in methods:
339 if not name.startswith("_") and callable(getattr(resource, name)):
340 doc = getattr(resource, name).__doc__
341 params = method_params(doc)
342 firstline = doc.splitlines()[0]
343 html.append(
344 string.Template(METHOD_LINK).substitute(
345 name=name, params=params, firstline=firstline
346 )
347 )
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400348
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700349 if methods:
350 html.append("<h3>Method Details</h3>")
351 for name in methods:
352 dname = name.rsplit("_")[0]
353 html.append(method(name, getattr(resource, name).__doc__))
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400354
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700355 html.append("</body></html>")
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400356
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700357 return "\n".join(html)
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400358
359
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400360def document_collection_recursive(resource, path, root_discovery, discovery, doc_destination_dir):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700361 html = document_collection(resource, path, root_discovery, discovery)
Joe Gregorioafc45f22011-02-20 16:11:28 -0500362
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400363 f = open(pathlib.Path(doc_destination_dir).joinpath(path + "html"), "w")
Billy SU84d45612020-04-21 06:15:56 +0800364
365 f.write(html)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700366 f.close()
Joe Gregorioafc45f22011-02-20 16:11:28 -0500367
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700368 for name in dir(resource):
369 if (
370 not name.startswith("_")
371 and callable(getattr(resource, name))
372 and hasattr(getattr(resource, name), "__is_resource__")
373 and discovery != {}
374 ):
375 dname = name.rsplit("_")[0]
376 collection = getattr(resource, name)()
377 document_collection_recursive(
378 collection,
379 path + name + ".",
380 root_discovery,
381 discovery["resources"].get(dname, {}),
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400382 doc_destination_dir
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700383 )
384
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400385
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400386def document_api(name, version, uri, doc_destination_dir):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700387 """Document the given API.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400388
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400389 Args:
390 name (str): Name of the API.
391 version (str): Version of the API.
392 uri (str): URI of the API's discovery document
393 doc_destination_dir (str): relative path where the reference
394 documentation should be saved.
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400395 """
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400396 http = build_http()
397 resp, content = http.request(
398 uri or uritemplate.expand(
399 FLAGS.discovery_uri_template, {"api": name, "apiVersion": version}
400 )
401 )
Jon Wayne Parrottfd2f99c2016-02-19 16:02:04 -0800402
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400403 if resp.status == 200:
404 discovery = json.loads(content)
405 service = build_from_document(discovery)
Anthonios Partheniou31bbe512021-05-27 16:49:17 -0400406 doc_name = "{}.{}.json".format(name, version)
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400407 discovery_file_path = DISCOVERY_DOC_DIR / doc_name
408 revision = None
409
410 pathlib.Path(discovery_file_path).touch(exist_ok=True)
411
412 # Write discovery artifact to disk if revision equal or newer
413 with open(discovery_file_path, "r+") as f:
414 try:
415 json_data = json.load(f)
416 revision = json_data['revision']
417 except json.JSONDecodeError:
418 revision = None
419
420 if revision is None or discovery['revision'] >= revision:
421 # Reset position to the beginning
422 f.seek(0)
423 # Write the changes to disk
424 json.dump(discovery, f, indent=2, sort_keys=True)
425 # Truncate anything left as it's not needed
426 f.truncate()
427
428 elif resp.status == 404:
429 print("Warning: {} {} not found. HTTP Code: {}".format(name, version, resp.status))
430 return
431 else:
432 print("Warning: {} {} could not be built. HTTP Code: {}".format(name, version, resp.status))
433 return
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400434
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700435 document_collection_recursive(
Anthonios Partheniou31bbe512021-05-27 16:49:17 -0400436 service, "{}_{}.".format(name, safe_version(version)), discovery, discovery, doc_destination_dir
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700437 )
Joe Gregorio81d92cc2012-07-09 16:46:02 -0400438
Joe Gregorioafc45f22011-02-20 16:11:28 -0500439
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400440def document_api_from_discovery_document(discovery_url, doc_destination_dir):
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700441 """Document the given API.
Joe Gregoriobb964352013-03-03 20:45:29 -0500442
443 Args:
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400444 discovery_url (str): URI of discovery document.
445 doc_destination_dir (str): relative path where the reference
446 documentation should be saved.
Joe Gregoriobb964352013-03-03 20:45:29 -0500447 """
Igor Maravić22435292017-01-19 22:28:22 +0100448 http = build_http()
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400449 response, content = http.request(discovery_url)
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700450 discovery = json.loads(content)
Bu Sun Kimc9773042019-07-17 14:03:17 -0700451
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700452 service = build_from_document(discovery)
Bu Sun Kimc9773042019-07-17 14:03:17 -0700453
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700454 name = discovery["version"]
455 version = safe_version(discovery["version"])
Bu Sun Kimc9773042019-07-17 14:03:17 -0700456
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700457 document_collection_recursive(
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400458 service, "{}_{}.".format(name, version), discovery, discovery, doc_destination_dir
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700459 )
460
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400461def generate_all_api_documents(directory_uri=DIRECTORY_URI, doc_destination_dir=BASE):
462 """ Retrieve discovery artifacts and fetch reference documentations
463 for all apis listed in the public discovery directory.
464 args:
465 directory_uri (str): uri of the public discovery directory.
466 doc_destination_dir (str): relative path where the reference
467 documentation should be saved.
468 """
469 api_directory = collections.defaultdict(list)
470 http = build_http()
471 resp, content = http.request(directory_uri)
472 if resp.status == 200:
473 directory = json.loads(content)["items"]
474 for api in directory:
475 document_api(api["name"], api["version"], api["discoveryRestUrl"], doc_destination_dir)
476 api_directory[api["name"]].append(api["version"])
477
478 # sort by api name and version number
479 for api in api_directory:
480 api_directory[api] = sorted(api_directory[api])
481 api_directory = OrderedDict(
482 sorted(api_directory.items(), key=lambda x: x[0])
483 )
484
485 markdown = []
486 for api, versions in api_directory.items():
487 markdown.append("## %s" % api)
488 for version in versions:
489 markdown.append(
490 "* [%s](http://googleapis.github.io/google-api-python-client/docs/dyn/%s_%s.html)"
491 % (version, api, safe_version(version))
492 )
493 markdown.append("\n")
494
495 with open(BASE / "index.md", "w") as f:
496 markdown = "\n".join(markdown)
497 f.write(markdown)
498
499 else:
500 sys.exit("Failed to load the discovery document.")
Bu Sun Kim66bb32c2019-10-30 10:11:58 -0700501
502if __name__ == "__main__":
503 FLAGS = parser.parse_args(sys.argv[1:])
504 if FLAGS.discovery_uri:
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400505 document_api_from_discovery_document(discovery_url=FLAGS.discovery_uri, doc_destination_dir=FLAGS.dest)
Joe Gregoriobb964352013-03-03 20:45:29 -0500506 else:
Anthonios Partheniouaff037a2021-04-21 11:00:09 -0400507 generate_all_api_documents(directory_uri=FLAGS.directory_uri, doc_destination_dir=FLAGS.dest)