hsds 0.9.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. admin/__init__.py +0 -0
  2. admin/config/config.yml +106 -0
  3. hsds/__init__.py +0 -0
  4. hsds/app.py +385 -0
  5. hsds/async_lib.py +536 -0
  6. hsds/attr_dn.py +619 -0
  7. hsds/attr_sn.py +1436 -0
  8. hsds/basenode.py +711 -0
  9. hsds/chunk_crawl.py +915 -0
  10. hsds/chunk_dn.py +889 -0
  11. hsds/chunk_sn.py +1482 -0
  12. hsds/chunklocator.py +234 -0
  13. hsds/config.py +189 -0
  14. hsds/ctype_dn.py +196 -0
  15. hsds/ctype_sn.py +302 -0
  16. hsds/datanode.py +446 -0
  17. hsds/datanode_lib.py +1385 -0
  18. hsds/domain_crawl.py +567 -0
  19. hsds/domain_dn.py +228 -0
  20. hsds/domain_sn.py +1603 -0
  21. hsds/dset_dn.py +308 -0
  22. hsds/dset_lib.py +1047 -0
  23. hsds/dset_sn.py +1202 -0
  24. hsds/folder_crawl.py +131 -0
  25. hsds/group_dn.py +337 -0
  26. hsds/group_sn.py +295 -0
  27. hsds/headnode.py +516 -0
  28. hsds/hsds_app.py +402 -0
  29. hsds/hsds_logger.py +207 -0
  30. hsds/link_dn.py +486 -0
  31. hsds/link_sn.py +774 -0
  32. hsds/node_runner.py +53 -0
  33. hsds/servicenode.py +328 -0
  34. hsds/servicenode_lib.py +1205 -0
  35. hsds/util/__init__.py +13 -0
  36. hsds/util/arrayUtil.py +731 -0
  37. hsds/util/attrUtil.py +76 -0
  38. hsds/util/authUtil.py +663 -0
  39. hsds/util/awsLambdaClient.py +195 -0
  40. hsds/util/azureBlobClient.py +521 -0
  41. hsds/util/boolparser.py +293 -0
  42. hsds/util/chunkUtil.py +1551 -0
  43. hsds/util/domainUtil.py +325 -0
  44. hsds/util/dsetUtil.py +1063 -0
  45. hsds/util/fileClient.py +427 -0
  46. hsds/util/globparser.py +129 -0
  47. hsds/util/hdf5dtype.py +869 -0
  48. hsds/util/httpUtil.py +737 -0
  49. hsds/util/idUtil.py +540 -0
  50. hsds/util/jwtUtil.py +210 -0
  51. hsds/util/k8sClient.py +228 -0
  52. hsds/util/linkUtil.py +134 -0
  53. hsds/util/lruCache.py +404 -0
  54. hsds/util/query_marathon.py +114 -0
  55. hsds/util/rangegetUtil.py +159 -0
  56. hsds/util/s3Client.py +694 -0
  57. hsds/util/storUtil.py +706 -0
  58. hsds/util/timeUtil.py +83 -0
  59. hsds-0.9.1.dist-info/LICENSE +208 -0
  60. hsds-0.9.1.dist-info/METADATA +57 -0
  61. hsds-0.9.1.dist-info/RECORD +64 -0
  62. hsds-0.9.1.dist-info/WHEEL +5 -0
  63. hsds-0.9.1.dist-info/entry_points.txt +7 -0
  64. hsds-0.9.1.dist-info/top_level.txt +2 -0
admin/__init__.py ADDED
File without changes
@@ -0,0 +1,106 @@
1
+ # HSDS configuration
2
+ allow_noauth: true # enable unauthenticated requests
3
+ auth_expiration: -1 # set an expiration for credential caching
4
+ default_public: False # new domains are publically readable by default
5
+ aws_access_key_id: xxx # Replace with access key for account or use aws_iam_role
6
+ aws_secret_access_key: xxx # Replace with secret key for account
7
+ aws_iam_role: hsds_role # For EC2 using IAM roles
8
+ aws_region: us-east-1
9
+ hsds_endpoint: http://hsds.hdf.test # used for hateos links in response
10
+ aws_s3_gateway: null # use endpoint for the region HSDS is running in, e.g. 'https://s3.amazonaws.com' for us-east-1
11
+ aws_s3_no_sign_request: false # do not use credentials for S3 requests, equivalent of --no-sign-request for AWS CLI
12
+ aws_dynamodb_gateway: null # use for dynamodb endpint, e.g. 'https://dynamodb.us-east-1.amazonaws.com',
13
+ aws_dynamodb_users_table: null # set to table name if dynamodb is used to store usernames and passwords
14
+ azure_connection_string: null # use for connecting to Azure blob storage
15
+ azure_resource_id: null # resource id for use with Azure Active Directory
16
+ azure_storage_account: null # storage account to use on Azure
17
+ azure_resource_group: null # Azure resource group the container (BUCKET_NAME) belongs to
18
+ root_dir: null # base directory to use for Posix storage
19
+ password_salt: null # salt value to generate password based on username. Not recommended for public deployments
20
+ bucket_name: hsdstest # set to use a default bucket, otherwise bucket param is needed for all requests
21
+ head_port: 5100 # port to use for head node
22
+ head_ram: 512m # memory for head container
23
+ dn_port: 6101 # Start dn ports at 6101
24
+ dn_ram: 3g # memory for DN container (per container)
25
+ sn_port: 5101 # Start sn ports at 5101
26
+ sn_ram: 3g # memory for SN container
27
+ target_sn_count: 0 # desired number of SN containers
28
+ target_dn_count: 0 # desire number of DN containers
29
+ log_level: INFO # log level. One of ERROR, WARNING, INFO, DEBUG
30
+ log_timestamps: false # emit timestamp with log messages
31
+ log_prefix: null # Prefix text to append to log entries
32
+ max_tcp_connections: 100 # max number of inflight tcp connections
33
+ head_sleep_time: 10 # max sleep time between health checks for head node
34
+ node_sleep_time: 10 # max sleep time between health checks for SN/DN nodes
35
+ async_sleep_time: 1 # max sleep time between async task runs
36
+ scan_sleep_time: 10 # max sleep time between scanning runs
37
+ scan_wait_time: 10 # min time to wait after a domain update before starting a scan
38
+ max_scan_duration: 180 # max time to wait for a scan to complete before raising error
39
+ gc_sleep_time: 10 # max time between runs to delete unused objects
40
+ s3_sync_interval: 1 # time to wait between s3_sync checks (in sec)
41
+ s3_age_time: 1 # time to wait since last update to write an object to S3
42
+ s3_sync_task_timeout: 10 # time to cancel write task if no response
43
+ store_read_timeout: 1 # time to cancel storage read request if no response
44
+ store_read_sleep_interval: 0.1 # time to sleep between checking on read request
45
+ max_pending_write_requests: 20 # maxium number of inflight write requests
46
+ flush_sleep_interval: 1 # time to wait between checking on dirty objects
47
+ flush_timeout: 10 # max time to wait on all I/O operations to complete for a flush
48
+ min_chunk_size: 1m # 1 MB
49
+ max_chunk_size: 4m # 4 MB
50
+ max_request_size: 100m # 100 MB - should be no smaller than client_max_body_size in nginx tmpl (if using nginx)
51
+ max_chunks_per_folder: 0 # max number of chunks per s3 folder. 0 for unlimiited
52
+ max_task_count: 100 # maximum number of concurrent tasks per node before server will return 503 error
53
+ max_tasks_per_node_per_request: 16 # maximum number of inflight tasks to each node per request
54
+ aio_max_pool_connections: 64 # number of connections to keep in conection pool for aiobotocore requests
55
+ client_pool_count: 10 # pool count for SessionClient
56
+ metadata_mem_cache_size: 128m # 128 MB - metadata cache size per DN node
57
+ metadata_mem_cache_expire: 3600 # expire cache items after one hour
58
+ chunk_mem_cache_size: 128m # 128 MB - chunk cache size per DN node
59
+ chunk_mem_cache_expire: 3600 # expire cache items after one hour
60
+ timeout: 30 # http timeout - 30 sec
61
+ password_file: /config/passwd.txt # filepath to a text file of username/passwords. set to '' for no-auth access
62
+ groups_file: /config/groups.txt # filepath to text file defining user groups
63
+ server_name: Highly Scalable Data Service (HSDS) # this gets returned in the about request
64
+ greeting: Welcome to HSDS!
65
+ about: HSDS is a webservice for HDF data
66
+ top_level_domains: [] # list of possible top-level domains, example: ["/home", "/shared"], if empty all top-level folders in default bucket will be returned
67
+ cors_domain: "*" # domains allowed for CORS
68
+ admin_user: admin # user with admin privileges
69
+ admin_group: null # enable admin privileges for any user in this group
70
+ openid_provider: azure # OpenID authentication provider
71
+ openid_url: null # OpenID connect endpoint if provider is not azure or google
72
+ openid_audience: null # OpenID audience. This is synonymous with azure_resource_id for azure
73
+ openid_claims: unique_name,appid,roles # Comma seperated list of claims to resolve to usernames.
74
+ chaos_die: 0 # if > 0, have nodes randomly die after n seconds (for testing)
75
+ standalone_app: false # True when run as a single application
76
+ blosc_nthreads: 2 # number of threads to use for blosc compression. Set to 0 to have blosc auto-determine thread count
77
+ http_compression: false # Use HTTP compression
78
+ http_max_url_length: 512 # Limit http request url + params to be less than this
79
+ http_streaming: true # enable HTTP streaming
80
+ k8s_dn_label_selector: app=hsds # Selector for getting data node pods from a k8s deployment (https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors)
81
+ k8s_namespace: null # Specifies if a the client should be limited to a specific namespace. Useful for some RBAC configurations.
82
+ restart_policy: on-failure # Docker restart policy
83
+ # the following two values with give backoff times of approx: 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8
84
+ dn_max_retries: 7 # number of time to retry DN requests
85
+ dn_retry_backoff_exp: 0.1 # backoff factor for retries
86
+ xss_protection: "1; mode=block" # Include in response headers if set
87
+ allow_any_bucket_read: true # enable reads to buckets other than default bucket
88
+ allow_any_bucket_write: true # enable writes to buckets other than default bucket
89
+ bit_shuffle_default_blocksize: 2048 # default blocksize for bitshuffle filter
90
+ max_rangeget_gap: 1024 # max gap in byte for intelligent range get requests
91
+ # DEPRECATED - the remaining config values are not used in currently but kept for backward compatibility with older container images
92
+ aws_lambda_chunkread_function: null # name of aws lambda function for chunk reading
93
+ aws_lambda_threshold: 4 # number of chunks per node per request to reach before using lambda
94
+ aws_lambda_max_invoke: 1000 # max number of lambda functions to invoke simultaneously
95
+ aws_lambda_gateway: null # use lambda endpoint for region HSDS is running in
96
+ k8s_app_label: null # The app label for k8s deployments (use k8s_dn_label_selector instead)
97
+ write_zero_chunks: False # write chunk to storage even when it's all zeros (or in general equal to the fill value)
98
+ max_chunks_per_request: 1000 # maximum number of chunks to be serviced by one request
99
+ rangeget_port: 6900 # singleton proxy at port 6900
100
+ rangeget_ram: 2g # memory for RANGEGET container
101
+ data_cache_size: 128m # cache for rangegets
102
+ data_cache_max_req_size: 128k # max size for rangeget fetches
103
+ data_cache_expire_time: 3600 # expire cache items after one hour
104
+ data_cache_page_size: 4m # page size for range get cache, set to zero to disable proxy
105
+ data_cache_max_concurrent_read: 16 # maximum number of inflight storage read requests
106
+ domain_req_max_objects_limit: 500 # maximum number of objects to return in GET domain request with use_cache
hsds/__init__.py ADDED
File without changes
hsds/app.py ADDED
@@ -0,0 +1,385 @@
1
+ ##############################################################################
2
+ # Copyright by The HDF Group. #
3
+ # All rights reserved. #
4
+ # #
5
+ # This file is part of HSDS (HDF5 Scalable Data Service), Libraries and #
6
+ # Utilities. The full HSDS copyright notice, including #
7
+ # terms governing use, modification, and redistribution, is contained in #
8
+ # the file COPYING, which can be found at the root of the source code #
9
+ # distribution tree. If you do not have access to this file, you may #
10
+ # request a copy from help@hdfgroup.org. #
11
+ ##############################################################################
12
+ import argparse
13
+ import os
14
+ import json
15
+ import sys
16
+ import logging
17
+ import time
18
+
19
+ from .hsds_app import HsdsApp
20
+ from . import config
21
+
22
+ _HELP_USAGE = "Starts HSDS, a REST-based service for HDF5 data."
23
+
24
+ _HELP_EPILOG = """Examples:
25
+
26
+ - with a POSIX-based storage using a directory: ./hsdata for storage:
27
+
28
+ hsds --root_dir ~/hsdata
29
+
30
+ - with POSIX-based storage and config settings and password file:
31
+
32
+ hsds --root_dir ~/hsdata --password_file ./admin/config/passwd.txt \
33
+ --config_dir ./admin/config
34
+
35
+ - with minio data storage:
36
+
37
+ hsds --s3-gateway http://localhost:6007 --access-key-id demo:demo
38
+ --secret-access-key DEMO_PASS --password_file ./admin/config/passwd.txt
39
+
40
+ - with AWS S3 storage and a bucket in the us-west-2 region:
41
+
42
+ hsds --s3-gateway http://s3.us-west-2.amazonaws.com --access-key-id ${AWS_ACCESS_KEY_ID} \
43
+ --secret-access-key ${AWS_SECRET_ACCESS_KEY} --password_file ./admin/config/passwd.txt
44
+
45
+ """
46
+
47
+ # maximum number of characters if socket directory is given
48
+ # Exceeding this can cause errors - see: https://github.com/HDFGroup/hsds/issues/129
49
+ # Underlying issue is reported here: https://bugs.python.org/issue32958
50
+ MAX_SOCKET_DIR_PATH_LEN = 63
51
+
52
+
53
+ class UserConfig:
54
+ """
55
+ User Config state
56
+ """
57
+
58
+ def __init__(self, config_file=None, **kwargs):
59
+ self._cfg = {}
60
+ if config_file:
61
+ self._config_file = config_file
62
+ elif os.path.isfile(".hscfg"):
63
+ self._config_file = ".hscfg"
64
+ else:
65
+ self._config_file = os.path.expanduser("~/.hscfg")
66
+ # process config file if found
67
+ if os.path.isfile(self._config_file):
68
+ line_number = 0
69
+ with open(self._config_file) as f:
70
+ for line in f:
71
+ line_number += 1
72
+ s = line.strip()
73
+ if not s:
74
+ continue
75
+ if s[0] == "#":
76
+ # comment line
77
+ continue
78
+ index = line.find("=")
79
+ if index <= 0:
80
+ print(
81
+ "config file: {} line: {} is not valid".format(
82
+ self._config_file, line_number
83
+ )
84
+ )
85
+ continue
86
+ k = line[:index].strip()
87
+ nlen = index + 1
88
+ v = line[nlen:].strip()
89
+ if v and v.upper() != "NONE":
90
+ self._cfg[k] = v
91
+ # override any config values with environment variable if found
92
+ for k in self._cfg.keys():
93
+ if k.upper() in os.environ:
94
+ self._cfg[k] = os.environ[k.upper()]
95
+
96
+ # finally update any values that are passed in to the constructor
97
+ for k in kwargs.keys():
98
+ self._cfg[k.upper()] = kwargs[k]
99
+
100
+ def __getitem__(self, name):
101
+ """Get a config item"""
102
+
103
+ # Load a variable from environment. It would have only been loaded in
104
+ # __init__ if it was also specified in the config file.
105
+ env_name = name.upper()
106
+ if name not in self._cfg and env_name in os.environ:
107
+ self._cfg[name] = os.environ[env_name]
108
+
109
+ return self._cfg[name]
110
+
111
+ def __setitem__(self, name, obj):
112
+ """set config item"""
113
+ self._cfg[name] = obj
114
+
115
+ def __delitem__(self, name):
116
+ """Delete option."""
117
+ del self._cfg[name]
118
+
119
+ def __len__(self):
120
+ return len(self._cfg)
121
+
122
+ def __iter__(self):
123
+ """Iterate over config names"""
124
+ keys = self._cfg.keys()
125
+ for key in keys:
126
+ yield key
127
+
128
+ def __contains__(self, name):
129
+ return name in self._cfg or name.upper() in os.environ
130
+
131
+ def __repr__(self):
132
+ return json.dumps(self._cfg)
133
+
134
+ def keys(self):
135
+ return self._cfg.keys()
136
+
137
+ def get(self, name, default=None):
138
+ if name in self:
139
+ return self[name]
140
+ else:
141
+ return default
142
+
143
+
144
+ def main():
145
+ parser = argparse.ArgumentParser(
146
+ formatter_class=argparse.RawTextHelpFormatter,
147
+ usage=_HELP_USAGE,
148
+ epilog=_HELP_EPILOG,
149
+ )
150
+
151
+ parser.add_argument(
152
+ "--root_dir",
153
+ type=str,
154
+ dest="root_dir",
155
+ help="Directory where to store the object store data",
156
+ )
157
+ parser.add_argument(
158
+ "--bucket_name",
159
+ type=str,
160
+ dest="bucket_name",
161
+ help='Name of the bucket to use (e.g., "hsds.test").',
162
+ )
163
+ parser.add_argument(
164
+ "--host", default="", type=str, dest="host", help="host name for url"
165
+ )
166
+ parser.add_argument(
167
+ "--hs_username",
168
+ type=str,
169
+ dest="hs_username",
170
+ help="username to be added to list of valid users",
171
+ default="",
172
+ )
173
+ parser.add_argument(
174
+ "--hs_password",
175
+ type=str,
176
+ dest="hs_password",
177
+ help="password for hs_username",
178
+ default="",
179
+ )
180
+ parser.add_argument(
181
+ "--password_file",
182
+ type=str,
183
+ dest="password_file",
184
+ help="location of hsds password file",
185
+ default="",
186
+ )
187
+
188
+ parser.add_argument(
189
+ "--logfile",
190
+ default="",
191
+ type=str,
192
+ dest="logfile",
193
+ help="filename for logout (default stdout).",
194
+ )
195
+ parser.add_argument(
196
+ "--loglevel",
197
+ default="",
198
+ type=str,
199
+ dest="loglevel",
200
+ help="log verbosity: DEBUG, WARNING, INFO, OR ERROR",
201
+ )
202
+ parser.add_argument(
203
+ "-p", "--port", default=0, type=int, dest="port", help="Service node port"
204
+ )
205
+ parser.add_argument(
206
+ "--count",
207
+ default=4,
208
+ type=int,
209
+ dest="dn_count",
210
+ help="Number of dn sub-processes to create.",
211
+ )
212
+ parser.add_argument(
213
+ "--socket_dir",
214
+ default="",
215
+ type=str,
216
+ dest="socket_dir",
217
+ help="directory for socket endpoint",
218
+ )
219
+ parser.add_argument(
220
+ "--config_dir",
221
+ default="",
222
+ type=str,
223
+ dest="config_dir",
224
+ help="directory for config data",
225
+ )
226
+
227
+ args = parser.parse_args()
228
+
229
+ kwargs = {} # options to pass to hsdsapp
230
+
231
+ sys.stdout.reconfigure(encoding='utf-8')
232
+ sys.stderr.reconfigure(encoding='utf-8')
233
+
234
+ # setup logging
235
+ if args.loglevel:
236
+ log_level_cfg = args.loglevel
237
+ kwargs["log_level"] = args.loglevel
238
+ elif "LOG_LEVEL" in os.environ:
239
+ log_level_cfg = os.environ["LOG_LEVEL"]
240
+ else:
241
+ log_level_cfg = "INFO"
242
+ if log_level_cfg == "DEBUG":
243
+ log_level = logging.DEBUG
244
+ elif log_level_cfg == "INFO":
245
+ log_level = logging.INFO
246
+ elif log_level_cfg in ("WARN", "WARNING"):
247
+ log_level = logging.WARN
248
+ elif log_level_cfg == "ERROR":
249
+ log_level = logging.ERROR
250
+ else:
251
+ print(f"unsupported log_level: {log_level_cfg}, using INFO instead")
252
+ log_level = logging.INFO
253
+
254
+ print("set logging to::", log_level)
255
+ logging.basicConfig(level=log_level)
256
+
257
+ userConfig = UserConfig()
258
+
259
+ login_username = None
260
+ try:
261
+ login_username = os.getlogin()
262
+ except OSError:
263
+ pass # ignore
264
+
265
+ username = None
266
+ password = None
267
+
268
+ # validate password_file arg if used
269
+ if args.password_file:
270
+ if not os.path.isfile(args.password_file):
271
+ sys.exit(f"password file: {args.password_file} not found")
272
+ kwargs["password_file"] = args.password_file
273
+
274
+ if args.hs_username:
275
+ sys.exit("username arg cannot be used if password_file is set")
276
+ if args.hs_password:
277
+ sys.exit("password arg cannot be used if password_file is set")
278
+ else:
279
+ if args.hs_username:
280
+ username = args.hs_username
281
+ elif "HS_USERNAME" in userConfig:
282
+ # can set username based on HS_USERNAME environment variable
283
+ username = userConfig["HS_USERNAME"]
284
+ else:
285
+ username = login_username
286
+
287
+ if args.hs_password:
288
+ password = args.hs_password
289
+ elif "HS_PASSWORD" in userConfig:
290
+ # can set password based on HS_PASSWORD environment variable
291
+ password = userConfig["HS_PASSWORD"]
292
+ else:
293
+ password = login_username
294
+
295
+ if username and password:
296
+ kwargs["username"] = username
297
+ kwargs["password"] = password
298
+
299
+ # use unix domain socket if a socket dir is set
300
+ if args.socket_dir:
301
+ socket_dir = os.path.abspath(args.socket_dir)
302
+ if not os.path.isdir(socket_dir):
303
+ raise FileNotFoundError(f"directory: {socket_dir} not found")
304
+ kwargs["socket_dir"] = socket_dir
305
+ else:
306
+ # USE TCP connect
307
+ if args.host:
308
+ kwargs["host"] = args.host
309
+ else:
310
+ kwargs["host"] = "localhost"
311
+ # sn_port only relevant for TCP connections
312
+ if args.port:
313
+ kwargs["sn_port"] = args.port
314
+ else:
315
+ kwargs["sn_port"] = 5101 # TBD - use config
316
+
317
+ if args.logfile:
318
+ logfile = os.path.abspath(args.logfile)
319
+ elif args.host:
320
+ logfile = os.path.abspath("hs.log")
321
+ else:
322
+ socket_dir = os.path.abspath(args.socket_dir)
323
+ logfile = os.path.join(socket_dir, "hs.log")
324
+ print("logfile:", logfile)
325
+ kwargs["logfile"] = logfile
326
+
327
+ if args.root_dir:
328
+ kwargs["root_dir"] = args.root_dir
329
+
330
+ config_dir = None
331
+ if args.config_dir:
332
+ if not os.path.isdir(args.config_dir):
333
+ print(f"config_dir: {args.config_dir} not found")
334
+ else:
335
+ config_dir = args.config_dir
336
+ if config_dir:
337
+ kwargs["config_dir"] = config_dir
338
+
339
+ if args.dn_count:
340
+ kwargs["dn_count"] = args.dn_count
341
+
342
+ if args.bucket_name:
343
+ bucket_name = args.bucket_name
344
+ else:
345
+ bucket_name = config.get("bucket_name")
346
+ if not bucket_name:
347
+ sys.exit("bucket_name not set")
348
+ if args.root_dir:
349
+ root_dir = args.root_dir
350
+ else:
351
+ root_dir = config.get("root_dir")
352
+ if not root_dir:
353
+ # check that AWS_S3_GATEWAY or AZURE_CONNECTION_STRING is set
354
+ if not config.get("aws_s3_gateway") and not config.get("azure_connection_string"):
355
+ sys.exit("root_dir not set (and no S3 or Azure connection info)")
356
+ else:
357
+ if not os.path.isdir(root_dir):
358
+ sys.exit(f"directory: {root_dir} not found")
359
+ bucket_path = os.path.join(root_dir, bucket_name)
360
+ if not os.path.isdir(bucket_path):
361
+ os.mkdir(bucket_path)
362
+
363
+ app = HsdsApp(**kwargs)
364
+ app.run()
365
+
366
+ waiting_on_ready = True
367
+
368
+ while True:
369
+ try:
370
+ time.sleep(1)
371
+ app.check_processes()
372
+ except KeyboardInterrupt:
373
+ print("got keyboard interrupt")
374
+ break
375
+ except Exception as e:
376
+ print(f"got exception: {e}")
377
+ break
378
+ if waiting_on_ready and app.ready:
379
+ waiting_on_ready = False
380
+ print("")
381
+ print("READY! use endpoint:", app.endpoint)
382
+ print("")
383
+
384
+ print("shutting down server")
385
+ app.stop()