• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zappa / Zappa / 8767636494

20 Apr 2024 08:41PM UTC coverage: 74.655% (-0.2%) from 74.81%
8767636494

Pull #1325

github

web-flow
Merge 61f840741 into a38058b1b
Pull Request #1325: extend SQS event handler

4 of 9 new or added lines in 1 file covered. (44.44%)

3 existing lines in 1 file now uncovered.

2763 of 3701 relevant lines covered (74.66%)

3.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.18
/zappa/cli.py
1
"""
2
Zappa CLI
3

4
Deploy arbitrary Python programs as serverless Zappa applications.
5

6
"""
7

8
import argparse
5✔
9
import base64
5✔
10
import collections
5✔
11
import importlib
5✔
12
import inspect
5✔
13
import os
5✔
14
import pkgutil
5✔
15
import random
5✔
16
import re
5✔
17
import string
5✔
18
import sys
5✔
19
import tempfile
5✔
20
import time
5✔
21
import zipfile
5✔
22
from builtins import bytes, input
5✔
23
from datetime import datetime, timedelta
5✔
24
from typing import Optional
5✔
25

26
import argcomplete
5✔
27
import botocore
5✔
28
import click
5✔
29
import hjson as json
5✔
30
import pkg_resources
5✔
31
import requests
5✔
32
import slugify
5✔
33
import toml
5✔
34
import yaml
5✔
35
from click import BaseCommand, Context
5✔
36
from click.exceptions import ClickException
5✔
37
from click.globals import push_context
5✔
38
from dateutil import parser
5✔
39

40
from .core import API_GATEWAY_REGIONS, Zappa
5✔
41
from .utilities import (
5✔
42
    check_new_version_available,
43
    detect_django_settings,
44
    detect_flask_apps,
45
    get_runtime_from_python_version,
46
    get_venv_from_python_version,
47
    human_size,
48
    is_valid_bucket_name,
49
    parse_s3_url,
50
    string_to_timestamp,
51
    validate_name,
52
)
53

54
CUSTOM_SETTINGS = [
5✔
55
    "apigateway_policy",
56
    "assume_policy",
57
    "attach_policy",
58
    "aws_region",
59
    "delete_local_zip",
60
    "delete_s3_zip",
61
    "exclude",
62
    "exclude_glob",
63
    "extra_permissions",
64
    "include",
65
    "role_name",
66
    "touch",
67
]
68

69
BOTO3_CONFIG_DOCS_URL = "https://boto3.readthedocs.io/en/latest/guide/quickstart.html#configuration"
5✔
70

71

72
##
73
# Main Input Processing
74
##
75

76

77
class ZappaCLI:
5✔
78
    """
79
    ZappaCLI object is responsible for loading the settings,
80
    handling the input arguments and executing the calls to the core library.
81
    """
82

83
    # CLI
84
    vargs = None
5✔
85
    command = None
5✔
86
    stage_env = None
5✔
87

88
    # Zappa settings
89
    zappa = None
5✔
90
    zappa_settings = None
5✔
91
    load_credentials = True
5✔
92
    disable_progress = False
5✔
93

94
    # Specific settings
95
    api_stage = None
5✔
96
    app_function = None
5✔
97
    aws_region = None
5✔
98
    debug = None
5✔
99
    prebuild_script = None
5✔
100
    project_name = None
5✔
101
    profile_name = None
5✔
102
    lambda_arn = None
5✔
103
    lambda_name = None
5✔
104
    lambda_description = None
5✔
105
    lambda_concurrency = None
5✔
106
    s3_bucket_name = None
5✔
107
    settings_file = None
5✔
108
    zip_path = None
5✔
109
    handler_path = None
5✔
110
    vpc_config = None
5✔
111
    memory_size = None
5✔
112
    ephemeral_storage = None
5✔
113
    use_apigateway = None
5✔
114
    lambda_handler = None
5✔
115
    django_settings = None
5✔
116
    manage_roles = True
5✔
117
    exception_handler = None
5✔
118
    environment_variables = None
5✔
119
    authorizer = None
5✔
120
    xray_tracing = False
5✔
121
    aws_kms_key_arn = ""
5✔
122
    context_header_mappings = None
5✔
123
    additional_text_mimetypes = None
5✔
124
    tags = []  # type: ignore[var-annotated]
5✔
125
    layers = None
5✔
126

127
    stage_name_env_pattern = re.compile("^[a-zA-Z0-9_]+$")
5✔
128

129
    def __init__(self):
5✔
130
        self._stage_config_overrides = {}  # change using self.override_stage_config_setting(key, val)
5✔
131

132
    @property
5✔
133
    def stage_config(self):
5✔
134
        """
135
        A shortcut property for settings of a stage.
136
        """
137

138
        def get_stage_setting(stage, extended_stages=None):
5✔
139
            if extended_stages is None:
5✔
140
                extended_stages = []
5✔
141

142
            if stage in extended_stages:
5✔
143
                raise RuntimeError(
5✔
144
                    stage + " has already been extended to these settings. "
145
                    "There is a circular extends within the settings file."
146
                )
147
            extended_stages.append(stage)
5✔
148

149
            try:
5✔
150
                stage_settings = dict(self.zappa_settings[stage].copy())
5✔
151
            except KeyError:
5✔
152
                raise ClickException("Cannot extend settings for undefined stage '" + stage + "'.")
5✔
153

154
            extends_stage = self.zappa_settings[stage].get("extends", None)
5✔
155
            if not extends_stage:
5✔
156
                return stage_settings
5✔
157
            extended_settings = get_stage_setting(stage=extends_stage, extended_stages=extended_stages)
5✔
158
            extended_settings.update(stage_settings)
5✔
159
            return extended_settings
5✔
160

161
        settings = get_stage_setting(stage=self.api_stage)
5✔
162

163
        # Backwards compatible for delete_zip setting that was more explicitly named delete_local_zip
164
        if "delete_zip" in settings:
5✔
165
            settings["delete_local_zip"] = settings.get("delete_zip")
×
166

167
        settings.update(self.stage_config_overrides)
5✔
168

169
        return settings
5✔
170

171
    @property
5✔
172
    def stage_config_overrides(self):
5✔
173
        """
174
        Returns zappa_settings we forcefully override for the current stage
175
        set by `self.override_stage_config_setting(key, value)`
176
        """
177
        return getattr(self, "_stage_config_overrides", {}).get(self.api_stage, {})
5✔
178

179
    def override_stage_config_setting(self, key, val):
5✔
180
        """
181
        Forcefully override a setting set by zappa_settings (for the current stage only)
182
        :param key: settings key
183
        :param val: value
184
        """
185
        self._stage_config_overrides = getattr(self, "_stage_config_overrides", {})
5✔
186
        self._stage_config_overrides.setdefault(self.api_stage, {})[key] = val
5✔
187

188
    def handle(self, argv=None):
5✔
189
        """
190
        Main function.
191
        Parses command, load settings and dispatches accordingly.
192
        """
193

194
        desc = "Zappa - Deploy Python applications to AWS Lambda" " and API Gateway.\n"
×
195
        parser = argparse.ArgumentParser(description=desc)
×
196
        parser.add_argument(
×
197
            "-v",
198
            "--version",
199
            action="version",
200
            version=pkg_resources.get_distribution("zappa").version,
201
            help="Print the zappa version",
202
        )
203
        parser.add_argument("--color", default="auto", choices=["auto", "never", "always"])
×
204

205
        env_parser = argparse.ArgumentParser(add_help=False)
×
206
        me_group = env_parser.add_mutually_exclusive_group()
×
207
        all_help = "Execute this command for all of our defined " "Zappa stages."
×
208
        me_group.add_argument("--all", action="store_true", help=all_help)
×
209
        me_group.add_argument("stage_env", nargs="?")
×
210

211
        group = env_parser.add_argument_group()
×
212
        group.add_argument("-a", "--app_function", help="The WSGI application function.")
×
213
        group.add_argument("-s", "--settings_file", help="The path to a Zappa settings file.")
×
214
        group.add_argument("-q", "--quiet", action="store_true", help="Silence all output.")
×
215
        # https://github.com/Miserlou/Zappa/issues/407
216
        # Moved when 'template' command added.
217
        # Fuck Terraform.
218
        group.add_argument(
×
219
            "-j",
220
            "--json",
221
            action="store_true",
222
            help="Make the output of this command be machine readable.",
223
        )
224
        # https://github.com/Miserlou/Zappa/issues/891
225
        group.add_argument("--disable_progress", action="store_true", help="Disable progress bars.")
×
226
        group.add_argument("--no_venv", action="store_true", help="Skip venv check.")
×
227

228
        ##
229
        # Certify
230
        ##
231
        subparsers = parser.add_subparsers(title="subcommands", dest="command")
×
232
        cert_parser = subparsers.add_parser("certify", parents=[env_parser], help="Create and install SSL certificate")
×
233
        cert_parser.add_argument(
×
234
            "--manual",
235
            action="store_true",
236
            help=("Gets new Let's Encrypt certificates, but prints them to console." "Does not update API Gateway domains."),
237
        )
238
        cert_parser.add_argument("-y", "--yes", action="store_true", help="Auto confirm yes.")
×
239

240
        ##
241
        # Deploy
242
        ##
243
        deploy_parser = subparsers.add_parser("deploy", parents=[env_parser], help="Deploy application.")
×
244
        deploy_parser.add_argument(
×
245
            "-z",
246
            "--zip",
247
            help="Deploy Lambda with specific local or S3 hosted zip package",
248
        )
249
        deploy_parser.add_argument(
×
250
            "-d",
251
            "--docker-image-uri",
252
            help="Deploy Lambda with a specific docker image hosted in AWS Elastic Container Registry",
253
        )
254

255
        ##
256
        # Init
257
        ##
258
        subparsers.add_parser("init", help="Initialize Zappa app.")
×
259

260
        ##
261
        # Package
262
        ##
263
        package_parser = subparsers.add_parser(
×
264
            "package",
265
            parents=[env_parser],
266
            help="Build the application zip package locally.",
267
        )
268
        package_parser.add_argument("-o", "--output", help="Name of file to output the package to.")
×
269

270
        ##
271
        # Template
272
        ##
273
        template_parser = subparsers.add_parser(
×
274
            "template",
275
            parents=[env_parser],
276
            help="Create a CloudFormation template for this API Gateway.",
277
        )
278
        template_parser.add_argument(
×
279
            "-l",
280
            "--lambda-arn",
281
            required=True,
282
            help="ARN of the Lambda function to template to.",
283
        )
284
        template_parser.add_argument("-r", "--role-arn", required=True, help="ARN of the Role to template with.")
×
285
        template_parser.add_argument("-o", "--output", help="Name of file to output the template to.")
×
286

287
        ##
288
        # Invocation
289
        ##
290
        invoke_parser = subparsers.add_parser("invoke", parents=[env_parser], help="Invoke remote function.")
×
291
        invoke_parser.add_argument(
×
292
            "--raw",
293
            action="store_true",
294
            help=("When invoking remotely, invoke this python as a string," " not as a modular path."),
295
        )
296
        invoke_parser.add_argument("--no-color", action="store_true", help=("Don't color the output"))
×
297
        invoke_parser.add_argument("command_rest")
×
298

299
        ##
300
        # Manage
301
        ##
302
        manage_parser = subparsers.add_parser("manage", help="Invoke remote Django manage.py commands.")
×
303
        rest_help = "Command in the form of <env> <command>. <env> is not " "required if --all is specified"
×
304
        manage_parser.add_argument("--all", action="store_true", help=all_help)
×
305
        manage_parser.add_argument("command_rest", nargs="+", help=rest_help)
×
306
        manage_parser.add_argument("--no-color", action="store_true", help=("Don't color the output"))
×
307
        # This is explicitly added here because this is the only subcommand that doesn't inherit from env_parser
308
        # https://github.com/Miserlou/Zappa/issues/1002
309
        manage_parser.add_argument("-s", "--settings_file", help="The path to a Zappa settings file.")
×
310

311
        ##
312
        # Rollback
313
        ##
314
        def positive_int(s):
×
315
            """Ensure an arg is positive"""
316
            i = int(s)
×
317
            if i < 0:
×
318
                msg = "This argument must be positive (got {})".format(s)
×
319
                raise argparse.ArgumentTypeError(msg)
×
320
            return i
×
321

322
        rollback_parser = subparsers.add_parser(
×
323
            "rollback",
324
            parents=[env_parser],
325
            help="Rollback deployed code to a previous version.",
326
        )
327
        rollback_parser.add_argument(
×
328
            "-n",
329
            "--num-rollback",
330
            type=positive_int,
331
            default=1,
332
            help="The number of versions to rollback.",
333
        )
334

335
        ##
336
        # Scheduling
337
        ##
338
        subparsers.add_parser(
×
339
            "schedule",
340
            parents=[env_parser],
341
            help="Schedule functions to occur at regular intervals.",
342
        )
343

344
        ##
345
        # Status
346
        ##
347
        subparsers.add_parser(
×
348
            "status",
349
            parents=[env_parser],
350
            help="Show deployment status and event schedules.",
351
        )
352

353
        ##
354
        # Log Tailing
355
        ##
356
        tail_parser = subparsers.add_parser("tail", parents=[env_parser], help="Tail deployment logs.")
×
357
        tail_parser.add_argument("--no-color", action="store_true", help="Don't color log tail output.")
×
358
        tail_parser.add_argument(
×
359
            "--http",
360
            action="store_true",
361
            help="Only show HTTP requests in tail output.",
362
        )
363
        tail_parser.add_argument(
×
364
            "--non-http",
365
            action="store_true",
366
            help="Only show non-HTTP requests in tail output.",
367
        )
368
        tail_parser.add_argument(
×
369
            "--since",
370
            type=str,
371
            default="100000s",
372
            help="Only show lines since a certain timeframe.",
373
        )
374
        tail_parser.add_argument("--filter", type=str, default="", help="Apply a filter pattern to the logs.")
×
375
        tail_parser.add_argument(
×
376
            "--force-color",
377
            action="store_true",
378
            help="Force coloring log tail output even if coloring support is not auto-detected. (example: piping)",
379
        )
380
        tail_parser.add_argument(
×
381
            "--disable-keep-open",
382
            action="store_true",
383
            help="Exit after printing the last available log, rather than keeping the log open.",
384
        )
385

386
        ##
387
        # Undeploy
388
        ##
389
        undeploy_parser = subparsers.add_parser("undeploy", parents=[env_parser], help="Undeploy application.")
×
390
        undeploy_parser.add_argument(
×
391
            "--remove-logs",
392
            action="store_true",
393
            help=("Removes log groups of api gateway and lambda task" " during the undeployment."),
394
        )
395
        undeploy_parser.add_argument("-y", "--yes", action="store_true", help="Auto confirm yes.")
×
396

397
        ##
398
        # Unschedule
399
        ##
400
        subparsers.add_parser("unschedule", parents=[env_parser], help="Unschedule functions.")
×
401

402
        ##
403
        # Updating
404
        ##
405
        update_parser = subparsers.add_parser("update", parents=[env_parser], help="Update deployed application.")
×
406
        update_parser.add_argument(
×
407
            "-z",
408
            "--zip",
409
            help="Update Lambda with specific local or S3 hosted zip package",
410
        )
411
        update_parser.add_argument(
×
412
            "-n",
413
            "--no-upload",
414
            help="Update configuration where appropriate, but don't upload new code",
415
        )
416
        update_parser.add_argument(
×
417
            "-d",
418
            "--docker-image-uri",
419
            help="Update Lambda with a specific docker image hosted in AWS Elastic Container Registry",
420
        )
421

422
        ##
423
        # Debug
424
        ##
425
        subparsers.add_parser(
×
426
            "shell",
427
            parents=[env_parser],
428
            help="A debug shell with a loaded Zappa object.",
429
        )
430

431
        ##
432
        # Python Settings File
433
        ##
434
        settings_parser = subparsers.add_parser(
×
435
            "save-python-settings-file",
436
            parents=[env_parser],
437
            help="Generate & save the Zappa settings Python file for docker deployments",
438
        )
439
        settings_parser.add_argument(
×
440
            "-o",
441
            "--output_path",
442
            help=(
443
                "The path to save the Zappa settings Python file. "
444
                "File must be named zappa_settings.py and should be saved "
445
                "in the same directory as the Zappa handler.py"
446
            ),
447
        )
448

449
        argcomplete.autocomplete(parser)
×
450
        args = parser.parse_args(argv)
×
451
        self.vargs = vars(args)
×
452

453
        if args.color == "never":
×
454
            disable_click_colors()
×
455
        elif args.color == "always":
×
456
            # TODO: Support aggressive coloring like "--force-color" on all commands
457
            pass
×
458
        elif args.color == "auto":
×
459
            pass
460

461
        # Parse the input
462
        # NOTE(rmoe): Special case for manage command
463
        # The manage command can't have both stage_env and command_rest
464
        # arguments. Since they are both positional arguments argparse can't
465
        # differentiate the two. This causes problems when used with --all.
466
        # (e.g. "manage --all showmigrations admin" argparse thinks --all has
467
        # been specified AND that stage_env='showmigrations')
468
        # By having command_rest collect everything but --all we can split it
469
        # apart here instead of relying on argparse.
470
        if not args.command:
×
471
            parser.print_help()
×
472
            return
×
473

474
        if args.command == "manage" and not self.vargs.get("all"):
×
475
            self.stage_env = self.vargs["command_rest"].pop(0)
×
476
        else:
477
            self.stage_env = self.vargs.get("stage_env")
×
478

479
        if args.command in ("package", "save-python-settings-file"):
×
480
            self.load_credentials = False
×
481

482
        self.command = args.command
×
483

484
        self.disable_progress = self.vargs.get("disable_progress")
×
485
        if self.vargs.get("quiet"):
×
486
            self.silence()
×
487

488
        # We don't have any settings yet, so make those first!
489
        # (Settings-based interactions will fail
490
        # before a project has been initialized.)
491
        if self.command == "init":
×
492
            self.init()
×
493
            return
×
494

495
        # Make sure there isn't a new version available
496
        if not self.vargs.get("json"):
×
497
            self.check_for_update()
×
498

499
        # Load and Validate Settings File
500
        self.load_settings_file(self.vargs.get("settings_file"))
×
501

502
        # Should we execute this for all stages, or just one?
503
        all_stages = self.vargs.get("all")
×
504
        stages = []
×
505

506
        if all_stages:  # All stages!
×
507
            stages = self.zappa_settings.keys()
×
508
        else:  # Just one env.
509
            if not self.stage_env:
×
510
                # If there's only one stage defined in the settings,
511
                # use that as the default.
512
                if len(self.zappa_settings.keys()) == 1:
×
513
                    stages.append(list(self.zappa_settings.keys())[0])
×
514
                else:
515
                    parser.error("Please supply a stage to interact with.")
×
516
            else:
517
                stages.append(self.stage_env)
×
518

519
        for stage in stages:
×
520
            try:
×
521
                self.dispatch_command(self.command, stage)
×
522
            except ClickException as e:
×
523
                # Discussion on exit codes: https://github.com/Miserlou/Zappa/issues/407
524
                e.show()
×
525
                sys.exit(e.exit_code)
×
526

527
    def dispatch_command(self, command, stage):
5✔
528
        """
529
        Given a command to execute and stage,
530
        execute that command.
531
        """
532
        self.check_stage_name(stage)
5✔
533
        self.api_stage = stage
×
534

535
        if command not in ["status", "manage"]:
×
536
            if not self.vargs.get("json", None):
×
537
                click.echo(
×
538
                    "Calling "
539
                    + click.style(command, fg="green", bold=True)
540
                    + " for stage "
541
                    + click.style(self.api_stage, bold=True)
542
                    + ".."
543
                )
544

545
        # Explicitly define the app function.
546
        # Related: https://github.com/Miserlou/Zappa/issues/832
547
        if self.vargs.get("app_function", None):
×
548
            self.app_function = self.vargs["app_function"]
×
549

550
        # Load our settings, based on api_stage.
551
        try:
×
552
            self.load_settings(self.vargs.get("settings_file"))
×
553
        except ValueError as e:
×
554
            if hasattr(e, "message"):
×
555
                print("Error: {}".format(e.message))
×
556
            else:
557
                print(str(e))
×
558
            sys.exit(-1)
×
559
        self.callback("settings")
×
560

561
        # Hand it off
562
        if command == "deploy":  # pragma: no cover
563
            self.deploy(self.vargs["zip"], self.vargs["docker_image_uri"])
564
        if command == "package":  # pragma: no cover
565
            self.package(self.vargs["output"])
566
        if command == "template":  # pragma: no cover
567
            self.template(
568
                self.vargs["lambda_arn"],
569
                self.vargs["role_arn"],
570
                output=self.vargs["output"],
571
                json=self.vargs["json"],
572
            )
573
        elif command == "update":  # pragma: no cover
574
            self.update(
575
                self.vargs["zip"],
576
                self.vargs["no_upload"],
577
                self.vargs["docker_image_uri"],
578
            )
579
        elif command == "rollback":  # pragma: no cover
580
            self.rollback(self.vargs["num_rollback"])
581
        elif command == "invoke":  # pragma: no cover
582
            if not self.vargs.get("command_rest"):
583
                print("Please enter the function to invoke.")
584
                return
585

586
            self.invoke(
587
                self.vargs["command_rest"],
588
                raw_python=self.vargs["raw"],
589
                no_color=self.vargs["no_color"],
590
            )
591
        elif command == "manage":  # pragma: no cover
592
            if not self.vargs.get("command_rest"):
593
                print("Please enter the management command to invoke.")
594
                return
595

596
            if not self.django_settings:
597
                print("This command is for Django projects only!")
598
                print("If this is a Django project, please define django_settings in your zappa_settings.")
599
                return
600

601
            command_tail = self.vargs.get("command_rest")
602
            if len(command_tail) > 1:
603
                command = " ".join(command_tail)  # ex: zappa manage dev "shell --version"
604
            else:
605
                command = command_tail[0]  # ex: zappa manage dev showmigrations admin
606

607
            self.invoke(
608
                command,
609
                command="manage",
610
                no_color=self.vargs["no_color"],
611
            )
612

613
        elif command == "tail":  # pragma: no cover
614
            self.tail(
615
                colorize=(not self.vargs["no_color"]),
616
                http=self.vargs["http"],
617
                non_http=self.vargs["non_http"],
618
                since=self.vargs["since"],
619
                filter_pattern=self.vargs["filter"],
620
                force_colorize=self.vargs["force_color"] or None,
621
                keep_open=not self.vargs["disable_keep_open"],
622
            )
623
        elif command == "undeploy":  # pragma: no cover
624
            self.undeploy(no_confirm=self.vargs["yes"], remove_logs=self.vargs["remove_logs"])
625
        elif command == "schedule":  # pragma: no cover
626
            self.schedule()
627
        elif command == "unschedule":  # pragma: no cover
628
            self.unschedule()
629
        elif command == "status":  # pragma: no cover
630
            self.status(return_json=self.vargs["json"])
631
        elif command == "certify":  # pragma: no cover
632
            self.certify(no_confirm=self.vargs["yes"], manual=self.vargs["manual"])
633
        elif command == "shell":  # pragma: no cover
634
            self.shell()
635
        elif command == "save-python-settings-file":  # pragma: no cover
636
            self.save_python_settings_file(self.vargs["output_path"])
637

638
    ##
639
    # The Commands
640
    ##
641

642
    def save_python_settings_file(self, output_path=None):
5✔
643
        settings_path = output_path or "zappa_settings.py"
5✔
644
        print("Generating Zappa settings Python file and saving to {}".format(settings_path))
5✔
645
        if not settings_path.endswith("zappa_settings.py"):
5✔
646
            raise ValueError("Settings file must be named zappa_settings.py")
5✔
647
        zappa_settings_s = self.get_zappa_settings_string()
5✔
648
        with open(settings_path, "w") as f_out:
5✔
649
            f_out.write(zappa_settings_s)
5✔
650

651
    def package(self, output=None):
5✔
652
        """
653
        Only build the package
654
        """
655
        # Make sure we're in a venv.
656
        self.check_venv()
5✔
657

658
        # force not to delete the local zip
659
        self.override_stage_config_setting("delete_local_zip", False)
5✔
660
        # Execute the prebuild script
661
        if self.prebuild_script:
5✔
662
            self.execute_prebuild_script()
5✔
663
        # Create the Lambda Zip
664
        self.create_package(output)
5✔
665
        self.callback("zip")
5✔
666
        size = human_size(os.path.getsize(self.zip_path))
5✔
667
        click.echo(
5✔
668
            click.style("Package created", fg="green", bold=True)
669
            + ": "
670
            + click.style(self.zip_path, bold=True)
671
            + " ("
672
            + size
673
            + ")"
674
        )
675

676
    def template(self, lambda_arn, role_arn, output=None, json=False):
5✔
677
        """
678
        Only build the template file.
679
        """
680

681
        if not lambda_arn:
×
682
            raise ClickException("Lambda ARN is required to template.")
×
683

684
        if not role_arn:
×
685
            raise ClickException("Role ARN is required to template.")
×
686

687
        self.zappa.credentials_arn = role_arn
×
688

689
        # Create the template!
690
        template = self.zappa.create_stack_template(
×
691
            lambda_arn=lambda_arn,
692
            lambda_name=self.lambda_name,
693
            api_key_required=self.api_key_required,
694
            iam_authorization=self.iam_authorization,
695
            authorizer=self.authorizer,
696
            cors_options=self.cors,
697
            description=self.apigateway_description,
698
            endpoint_configuration=self.endpoint_configuration,
699
        )
700

701
        if not output:
×
702
            template_file = self.lambda_name + "-template-" + str(int(time.time())) + ".json"
×
703
        else:
704
            template_file = output
×
705
        with open(template_file, "wb") as out:
×
706
            out.write(bytes(template.to_json(indent=None, separators=(",", ":")), "utf-8"))
×
707

708
        if not json:
×
709
            click.echo(click.style("Template created", fg="green", bold=True) + ": " + click.style(template_file, bold=True))
×
710
        else:
711
            with open(template_file, "r") as out:
×
712
                print(out.read())
×
713

714
    def deploy(self, source_zip=None, docker_image_uri=None):
5✔
715
        """
716
        Package your project, upload it to S3, register the Lambda function
717
        and create the API Gateway routes.
718
        """
719

720
        if not source_zip or docker_image_uri:
5✔
721
            # Make sure the necessary IAM execution roles are available
722
            if self.manage_roles:
5✔
723
                try:
5✔
724
                    self.zappa.create_iam_roles()
5✔
725
                except botocore.client.ClientError as ce:
×
726
                    raise ClickException(
×
727
                        click.style("Failed", fg="red")
728
                        + " to "
729
                        + click.style("manage IAM roles", bold=True)
730
                        + "!\n"
731
                        + "You may "
732
                        + click.style("lack the necessary AWS permissions", bold=True)
733
                        + " to automatically manage a Zappa execution role.\n"
734
                        + click.style("Exception reported by AWS:", bold=True)
735
                        + format(ce)
736
                        + "\n"
737
                        + "To fix this, see here: "
738
                        + click.style(
739
                            "https://github.com/Zappa/Zappa#custom-aws-iam-roles-and-policies-for-deployment",
740
                            bold=True,
741
                        )
742
                        + "\n"
743
                    )
744

745
        # Make sure this isn't already deployed.
746
        deployed_versions = self.zappa.get_lambda_function_versions(self.lambda_name)
5✔
747
        if len(deployed_versions) > 0:
5✔
748
            raise ClickException(
×
749
                "This application is "
750
                + click.style("already deployed", fg="red")
751
                + " - did you mean to call "
752
                + click.style("update", bold=True)
753
                + "?"
754
            )
755

756
        if not source_zip and not docker_image_uri:
5✔
757
            # Make sure we're in a venv.
758
            self.check_venv()
5✔
759

760
            # Execute the prebuild script
761
            if self.prebuild_script:
5✔
762
                self.execute_prebuild_script()
5✔
763

764
            # Create the Lambda Zip
765
            self.create_package()
5✔
766
            self.callback("zip")
5✔
767

768
            # Upload it to S3
769
            success = self.zappa.upload_to_s3(
5✔
770
                self.zip_path,
771
                self.s3_bucket_name,
772
                disable_progress=self.disable_progress,
773
            )
774
            if not success:  # pragma: no cover
775
                raise ClickException("Unable to upload to S3. Quitting.")
776

777
            # If using a slim handler, upload it to S3 and tell lambda to use this slim handler zip
778
            if self.stage_config.get("slim_handler", False):
5✔
779
                # https://github.com/Miserlou/Zappa/issues/510
780
                success = self.zappa.upload_to_s3(
×
781
                    self.handler_path,
782
                    self.s3_bucket_name,
783
                    disable_progress=self.disable_progress,
784
                )
785
                if not success:  # pragma: no cover
786
                    raise ClickException("Unable to upload handler to S3. Quitting.")
787

788
                # Copy the project zip to the current project zip
789
                current_project_name = "{0!s}_{1!s}_current_project.tar.gz".format(self.api_stage, self.project_name)
×
790
                success = self.zappa.copy_on_s3(
×
791
                    src_file_name=self.zip_path,
792
                    dst_file_name=current_project_name,
793
                    bucket_name=self.s3_bucket_name,
794
                )
795
                if not success:  # pragma: no cover
796
                    raise ClickException("Unable to copy the zip to be the current project. Quitting.")
797

798
                handler_file = self.handler_path
×
799
            else:
800
                handler_file = self.zip_path
5✔
801

802
        # Fixes https://github.com/Miserlou/Zappa/issues/613
803
        try:
5✔
804
            self.lambda_arn = self.zappa.get_lambda_function(function_name=self.lambda_name)
5✔
805
        except botocore.client.ClientError:
×
806
            # Register the Lambda function with that zip as the source
807
            # You'll also need to define the path to your lambda_handler code.
808
            kwargs = dict(
×
809
                handler=self.lambda_handler,
810
                description=self.lambda_description,
811
                vpc_config=self.vpc_config,
812
                dead_letter_config=self.dead_letter_config,
813
                timeout=self.timeout_seconds,
814
                memory_size=self.memory_size,
815
                ephemeral_storage=self.ephemeral_storage,
816
                runtime=self.runtime,
817
                aws_environment_variables=self.aws_environment_variables,
818
                aws_kms_key_arn=self.aws_kms_key_arn,
819
                use_alb=self.use_alb,
820
                layers=self.layers,
821
                concurrency=self.lambda_concurrency,
822
            )
823
            kwargs["function_name"] = self.lambda_name
×
824
            if docker_image_uri:
×
825
                kwargs["docker_image_uri"] = docker_image_uri
×
826
            elif source_zip and source_zip.startswith("s3://"):
×
827
                bucket, key_name = parse_s3_url(source_zip)
×
828
                kwargs["bucket"] = bucket
×
829
                kwargs["s3_key"] = key_name
×
830
            elif source_zip and not source_zip.startswith("s3://"):
×
831
                with open(source_zip, mode="rb") as fh:
×
832
                    byte_stream = fh.read()
×
833
                kwargs["local_zip"] = byte_stream
×
834
            else:
835
                kwargs["bucket"] = self.s3_bucket_name
×
836
                kwargs["s3_key"] = handler_file
×
837

838
            self.lambda_arn = self.zappa.create_lambda_function(**kwargs)
×
839

840
        # Schedule events for this deployment
841
        self.schedule()
5✔
842

843
        endpoint_url = ""
5✔
844
        deployment_string = click.style("Deployment complete", fg="green", bold=True) + "!"
5✔
845

846
        if self.use_alb:
5✔
847
            kwargs = dict(
×
848
                lambda_arn=self.lambda_arn,
849
                lambda_name=self.lambda_name,
850
                alb_vpc_config=self.alb_vpc_config,
851
                timeout=self.timeout_seconds,
852
            )
853
            self.zappa.deploy_lambda_alb(**kwargs)
×
854

855
        if self.use_apigateway:
5✔
856
            # Create and configure the API Gateway
857
            self.zappa.create_stack_template(
5✔
858
                lambda_arn=self.lambda_arn,
859
                lambda_name=self.lambda_name,
860
                api_key_required=self.api_key_required,
861
                iam_authorization=self.iam_authorization,
862
                authorizer=self.authorizer,
863
                cors_options=self.cors,
864
                description=self.apigateway_description,
865
                endpoint_configuration=self.endpoint_configuration,
866
            )
867

868
            self.zappa.update_stack(
5✔
869
                self.lambda_name,
870
                self.s3_bucket_name,
871
                wait=True,
872
                disable_progress=self.disable_progress,
873
            )
874

875
            api_id = self.zappa.get_api_id(self.lambda_name)
5✔
876

877
            # Add binary support
878
            if self.binary_support:
5✔
879
                self.zappa.add_binary_support(api_id=api_id, cors=self.cors)
5✔
880

881
            # Add payload compression
882
            if self.stage_config.get("payload_compression", True):
5✔
883
                self.zappa.add_api_compression(
5✔
884
                    api_id=api_id,
885
                    min_compression_size=self.stage_config.get("payload_minimum_compression_size", 0),
886
                )
887

888
            # Deploy the API!
889
            endpoint_url = self.deploy_api_gateway(api_id)
5✔
890
            deployment_string = deployment_string + ": {}".format(endpoint_url)
5✔
891

892
            # Create/link API key
893
            if self.api_key_required:
5✔
894
                if self.api_key is None:
×
895
                    self.zappa.create_api_key(api_id=api_id, stage_name=self.api_stage)
×
896
                else:
897
                    self.zappa.add_api_stage_to_api_key(api_key=self.api_key, api_id=api_id, stage_name=self.api_stage)
×
898

899
            if self.stage_config.get("touch", True):
5✔
900
                self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name)
×
901
                self.touch_endpoint(endpoint_url)
×
902

903
        # Finally, delete the local copy our zip package
904
        if not source_zip and not docker_image_uri:
5✔
905
            if self.stage_config.get("delete_local_zip", True):
5✔
906
                self.remove_local_zip()
5✔
907

908
        # Remove the project zip from S3.
909
        if not source_zip and not docker_image_uri:
5✔
910
            self.remove_uploaded_zip()
5✔
911

912
        self.callback("post")
5✔
913

914
        click.echo(deployment_string)
5✔
915

916
    def update(self, source_zip=None, no_upload=False, docker_image_uri=None):
5✔
917
        """
918
        Repackage and update the function code.
919
        """
920

921
        if not source_zip and not docker_image_uri:
5✔
922
            # Make sure we're in a venv.
923
            self.check_venv()
5✔
924

925
            # Execute the prebuild script
926
            if self.prebuild_script:
5✔
927
                self.execute_prebuild_script()
5✔
928

929
            # Temporary version check
930
            try:
5✔
931
                updated_time = 1472581018
5✔
932
                function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name)
5✔
933
                conf = function_response["Configuration"]
5✔
934
                last_updated = parser.parse(conf["LastModified"])
5✔
935
                last_updated_unix = time.mktime(last_updated.timetuple())
5✔
936
            except botocore.exceptions.BotoCoreError as e:
×
937
                click.echo(click.style(type(e).__name__, fg="red") + ": " + e.args[0])
×
938
                sys.exit(-1)
×
939
            except Exception:
×
940
                click.echo(
×
941
                    click.style("Warning!", fg="red")
942
                    + " Couldn't get function "
943
                    + self.lambda_name
944
                    + " in "
945
                    + self.zappa.aws_region
946
                    + " - have you deployed yet?"
947
                )
948
                sys.exit(-1)
×
949

950
            if last_updated_unix <= updated_time:
5✔
951
                click.echo(
5✔
952
                    click.style("Warning!", fg="red")
953
                    + " You may have upgraded Zappa since deploying this application. You will need to "
954
                    + click.style("redeploy", bold=True)
955
                    + " for this deployment to work properly!"
956
                )
957

958
            # Make sure the necessary IAM execution roles are available
959
            if self.manage_roles:
5✔
960
                try:
5✔
961
                    self.zappa.create_iam_roles()
5✔
962
                except botocore.client.ClientError:
×
963
                    click.echo(click.style("Failed", fg="red") + " to " + click.style("manage IAM roles", bold=True) + "!")
×
964
                    click.echo(
×
965
                        "You may "
966
                        + click.style("lack the necessary AWS permissions", bold=True)
967
                        + " to automatically manage a Zappa execution role."
968
                    )
969
                    click.echo(
×
970
                        "To fix this, see here: "
971
                        + click.style(
972
                            "https://github.com/Zappa/Zappa#custom-aws-iam-roles-and-policies-for-deployment",
973
                            bold=True,
974
                        )
975
                    )
976
                    sys.exit(-1)
×
977

978
            # Create the Lambda Zip,
979
            if not no_upload:
5✔
980
                self.create_package()
5✔
981
                self.callback("zip")
5✔
982

983
            # Upload it to S3
984
            if not no_upload:
5✔
985
                success = self.zappa.upload_to_s3(
5✔
986
                    self.zip_path,
987
                    self.s3_bucket_name,
988
                    disable_progress=self.disable_progress,
989
                )
990
                if not success:  # pragma: no cover
991
                    raise ClickException("Unable to upload project to S3. Quitting.")
992

993
                # If using a slim handler, upload it to S3 and tell lambda to use this slim handler zip
994
                if self.stage_config.get("slim_handler", False):
5✔
995
                    # https://github.com/Miserlou/Zappa/issues/510
996
                    success = self.zappa.upload_to_s3(
×
997
                        self.handler_path,
998
                        self.s3_bucket_name,
999
                        disable_progress=self.disable_progress,
1000
                    )
1001
                    if not success:  # pragma: no cover
1002
                        raise ClickException("Unable to upload handler to S3. Quitting.")
1003

1004
                    # Copy the project zip to the current project zip
1005
                    current_project_name = "{0!s}_{1!s}_current_project.tar.gz".format(self.api_stage, self.project_name)
×
1006
                    success = self.zappa.copy_on_s3(
×
1007
                        src_file_name=self.zip_path,
1008
                        dst_file_name=current_project_name,
1009
                        bucket_name=self.s3_bucket_name,
1010
                    )
1011
                    if not success:  # pragma: no cover
1012
                        raise ClickException("Unable to copy the zip to be the current project. Quitting.")
1013

1014
                    handler_file = self.handler_path
×
1015
                else:
1016
                    handler_file = self.zip_path
5✔
1017

1018
        # Register the Lambda function with that zip as the source
1019
        # You'll also need to define the path to your lambda_handler code.
1020
        kwargs = dict(
5✔
1021
            bucket=self.s3_bucket_name,
1022
            function_name=self.lambda_name,
1023
            num_revisions=self.num_retained_versions,
1024
            concurrency=self.lambda_concurrency,
1025
        )
1026
        if docker_image_uri:
5✔
1027
            kwargs["docker_image_uri"] = docker_image_uri
×
1028
            self.lambda_arn = self.zappa.update_lambda_function(**kwargs)
×
1029
        elif source_zip and source_zip.startswith("s3://"):
5✔
1030
            bucket, key_name = parse_s3_url(source_zip)
×
1031
            kwargs.update(dict(bucket=bucket, s3_key=key_name))
×
1032
            self.lambda_arn = self.zappa.update_lambda_function(**kwargs)
×
1033
        elif source_zip and not source_zip.startswith("s3://"):
5✔
1034
            with open(source_zip, mode="rb") as fh:
×
1035
                byte_stream = fh.read()
×
1036
            kwargs["local_zip"] = byte_stream
×
1037
            self.lambda_arn = self.zappa.update_lambda_function(**kwargs)
×
1038
        else:
1039
            if not no_upload:
5✔
1040
                kwargs["s3_key"] = handler_file
5✔
1041
                self.lambda_arn = self.zappa.update_lambda_function(**kwargs)
5✔
1042

1043
        # Remove the uploaded zip from S3, because it is now registered..
1044
        if not source_zip and not no_upload and not docker_image_uri:
5✔
1045
            self.remove_uploaded_zip()
5✔
1046

1047
        # Update the configuration, in case there are changes.
1048
        self.lambda_arn = self.zappa.update_lambda_configuration(
5✔
1049
            lambda_arn=self.lambda_arn,
1050
            function_name=self.lambda_name,
1051
            handler=self.lambda_handler,
1052
            description=self.lambda_description,
1053
            vpc_config=self.vpc_config,
1054
            timeout=self.timeout_seconds,
1055
            memory_size=self.memory_size,
1056
            ephemeral_storage=self.ephemeral_storage,
1057
            runtime=self.runtime,
1058
            aws_environment_variables=self.aws_environment_variables,
1059
            aws_kms_key_arn=self.aws_kms_key_arn,
1060
            layers=self.layers,
1061
            wait=False,
1062
        )
1063

1064
        # Finally, delete the local copy our zip package
1065
        if not source_zip and not no_upload and not docker_image_uri:
5✔
1066
            if self.stage_config.get("delete_local_zip", True):
5✔
1067
                self.remove_local_zip()
5✔
1068

1069
        if self.use_apigateway:
5✔
1070
            self.zappa.create_stack_template(
5✔
1071
                lambda_arn=self.lambda_arn,
1072
                lambda_name=self.lambda_name,
1073
                api_key_required=self.api_key_required,
1074
                iam_authorization=self.iam_authorization,
1075
                authorizer=self.authorizer,
1076
                cors_options=self.cors,
1077
                description=self.apigateway_description,
1078
                endpoint_configuration=self.endpoint_configuration,
1079
            )
1080
            self.zappa.update_stack(
5✔
1081
                self.lambda_name,
1082
                self.s3_bucket_name,
1083
                wait=True,
1084
                update_only=True,
1085
                disable_progress=self.disable_progress,
1086
            )
1087

1088
            api_id = self.zappa.get_api_id(self.lambda_name)
5✔
1089

1090
            # Update binary support
1091
            if self.binary_support:
5✔
1092
                self.zappa.add_binary_support(api_id=api_id, cors=self.cors)
5✔
1093
            else:
1094
                self.zappa.remove_binary_support(api_id=api_id, cors=self.cors)
×
1095

1096
            if self.stage_config.get("payload_compression", True):
5✔
1097
                self.zappa.add_api_compression(
5✔
1098
                    api_id=api_id,
1099
                    min_compression_size=self.stage_config.get("payload_minimum_compression_size", 0),
1100
                )
1101
            else:
1102
                self.zappa.remove_api_compression(api_id=api_id)
×
1103

1104
            # It looks a bit like we might actually be using this just to get the URL,
1105
            # but we're also updating a few of the APIGW settings.
1106
            endpoint_url = self.deploy_api_gateway(api_id)
5✔
1107

1108
            if self.stage_config.get("domain", None):
5✔
1109
                endpoint_url = self.stage_config.get("domain")
×
1110

1111
        else:
1112
            endpoint_url = None
×
1113

1114
        self.schedule()
5✔
1115

1116
        # Update any cognito pool with the lambda arn
1117
        # do this after schedule as schedule clears the lambda policy and we need to add one
1118
        self.update_cognito_triggers()
5✔
1119

1120
        self.callback("post")
5✔
1121

1122
        if endpoint_url and "https://" not in endpoint_url:
5✔
1123
            endpoint_url = "https://" + endpoint_url
×
1124

1125
        if self.base_path:
5✔
1126
            endpoint_url += "/" + self.base_path
×
1127

1128
        deployed_string = "Your updated Zappa deployment is " + click.style("live", fg="green", bold=True) + "!"
5✔
1129
        if self.use_apigateway:
5✔
1130
            deployed_string = deployed_string + ": " + click.style("{}".format(endpoint_url), bold=True)
5✔
1131

1132
            api_url = None
5✔
1133
            if endpoint_url and "amazonaws.com" not in endpoint_url:
5✔
1134
                api_url = self.zappa.get_api_url(self.lambda_name, self.api_stage)
×
1135

1136
                if endpoint_url != api_url:
×
1137
                    deployed_string = deployed_string + " (" + api_url + ")"
×
1138

1139
            if self.stage_config.get("touch", True):
5✔
1140
                self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name)
×
1141
                if api_url:
×
1142
                    self.touch_endpoint(api_url)
×
1143
                elif endpoint_url:
×
1144
                    self.touch_endpoint(endpoint_url)
×
1145

1146
        click.echo(deployed_string)
5✔
1147

1148
    def rollback(self, revision):
5✔
1149
        """
1150
        Rollsback the currently deploy lambda code to a previous revision.
1151
        """
1152

1153
        print("Rolling back..")
5✔
1154

1155
        self.zappa.rollback_lambda_function_version(self.lambda_name, versions_back=revision)
5✔
1156
        print("Done!")
5✔
1157

1158
    def tail(
5✔
1159
        self,
1160
        since,
1161
        filter_pattern,
1162
        limit=10000,
1163
        keep_open=True,
1164
        colorize=True,
1165
        http=False,
1166
        non_http=False,
1167
        force_colorize=False,
1168
    ):
1169
        """
1170
        Tail this function's logs.
1171
        if keep_open, do so repeatedly, printing any new logs
1172
        """
1173

1174
        try:
5✔
1175
            since_stamp = string_to_timestamp(since)
5✔
1176

1177
            last_since = since_stamp
5✔
1178
            while True:
3✔
1179
                new_logs = self.zappa.fetch_logs(
5✔
1180
                    self.lambda_name,
1181
                    start_time=since_stamp,
1182
                    limit=limit,
1183
                    filter_pattern=filter_pattern,
1184
                )
1185

1186
                new_logs = [e for e in new_logs if e["timestamp"] > last_since]
5✔
1187
                self.print_logs(new_logs, colorize, http, non_http, force_colorize)
5✔
1188

1189
                if not keep_open:
5✔
1190
                    break
5✔
1191
                if new_logs:
×
1192
                    last_since = new_logs[-1]["timestamp"]
×
1193
                time.sleep(1)
×
1194
        except KeyboardInterrupt:  # pragma: no cover
1195
            # Die gracefully
1196
            try:
1197
                sys.exit(0)
1198
            except SystemExit:
1199
                os._exit(130)
1200

1201
    def undeploy(self, no_confirm=False, remove_logs=False):
5✔
1202
        """
1203
        Tear down an existing deployment.
1204
        """
1205

1206
        if not no_confirm:  # pragma: no cover
1207
            confirm = input("Are you sure you want to undeploy? [y/n] ")
1208
            if confirm != "y":
1209
                return
1210

1211
        if self.use_alb:
5✔
1212
            self.zappa.undeploy_lambda_alb(self.lambda_name)
×
1213

1214
        if self.use_apigateway:
5✔
1215
            if remove_logs:
5✔
1216
                self.zappa.remove_api_gateway_logs(self.lambda_name)
5✔
1217

1218
            domain_name = self.stage_config.get("domain", None)
5✔
1219
            base_path = self.stage_config.get("base_path", None)
5✔
1220

1221
            # Only remove the api key when not specified
1222
            if self.api_key_required and self.api_key is None:
5✔
1223
                api_id = self.zappa.get_api_id(self.lambda_name)
×
1224
                self.zappa.remove_api_key(api_id, self.api_stage)
×
1225

1226
            self.zappa.undeploy_api_gateway(self.lambda_name, domain_name=domain_name, base_path=base_path)
5✔
1227

1228
        self.unschedule()  # removes event triggers, including warm up event.
5✔
1229

1230
        self.zappa.delete_lambda_function(self.lambda_name)
5✔
1231
        if remove_logs:
5✔
1232
            self.zappa.remove_lambda_function_logs(self.lambda_name)
5✔
1233

1234
        click.echo(click.style("Done", fg="green", bold=True) + "!")
5✔
1235

1236
    def update_cognito_triggers(self):
5✔
1237
        """
1238
        Update any cognito triggers
1239
        """
1240
        if self.cognito:
5✔
1241
            user_pool = self.cognito.get("user_pool")
5✔
1242
            triggers = self.cognito.get("triggers", [])
5✔
1243
            lambda_configs = set()
5✔
1244
            for trigger in triggers:
5✔
1245
                lambda_configs.add(trigger["source"].split("_")[0])
5✔
1246
            self.zappa.update_cognito(self.lambda_name, user_pool, lambda_configs, self.lambda_arn)
5✔
1247

1248
    def schedule(self):
5✔
1249
        """
1250
        Given a a list of functions and a schedule to execute them,
1251
        setup up regular execution.
1252
        """
1253
        events = self.stage_config.get("events", [])
5✔
1254

1255
        if events:
5✔
1256
            if not isinstance(events, list):  # pragma: no cover
1257
                print("Events must be supplied as a list.")
1258
                return
1259

1260
        for event in events:
5✔
1261
            self.collision_warning(event.get("function"))
5✔
1262

1263
        if self.stage_config.get("keep_warm", True):
5✔
1264
            if not events:
5✔
1265
                events = []
×
1266

1267
            keep_warm_rate = self.stage_config.get("keep_warm_expression", "rate(4 minutes)")
5✔
1268
            events.append(
5✔
1269
                {
1270
                    "name": "zappa-keep-warm",
1271
                    "function": "handler.keep_warm_callback",
1272
                    "expression": keep_warm_rate,
1273
                    "description": "Zappa Keep Warm - {}".format(self.lambda_name),
1274
                }
1275
            )
1276

1277
        if events:
5✔
1278
            try:
5✔
1279
                function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name)
5✔
1280
            except botocore.exceptions.ClientError:  # pragma: no cover
1281
                click.echo(
1282
                    click.style("Function does not exist", fg="yellow")
1283
                    + ", please "
1284
                    + click.style("deploy", bold=True)
1285
                    + "first. Ex:"
1286
                    + click.style("zappa deploy {}.".format(self.api_stage), bold=True)
1287
                )
1288
                sys.exit(-1)
1289

1290
            print("Scheduling..")
5✔
1291
            self.zappa.schedule_events(
5✔
1292
                lambda_arn=function_response["Configuration"]["FunctionArn"],
1293
                lambda_name=self.lambda_name,
1294
                events=events,
1295
            )
1296

1297
        # Add async tasks SNS
1298
        if self.stage_config.get("async_source", None) == "sns" and self.stage_config.get("async_resources", True):
5✔
1299
            self.lambda_arn = self.zappa.get_lambda_function(function_name=self.lambda_name)
×
1300
            topic_arn = self.zappa.create_async_sns_topic(lambda_name=self.lambda_name, lambda_arn=self.lambda_arn)
×
1301
            click.echo("SNS Topic created: %s" % topic_arn)
×
1302

1303
        # Add async tasks DynamoDB
1304
        table_name = self.stage_config.get("async_response_table", False)
5✔
1305
        read_capacity = self.stage_config.get("async_response_table_read_capacity", 1)
5✔
1306
        write_capacity = self.stage_config.get("async_response_table_write_capacity", 1)
5✔
1307
        if table_name and self.stage_config.get("async_resources", True):
5✔
1308
            created, response_table = self.zappa.create_async_dynamodb_table(table_name, read_capacity, write_capacity)
×
1309
            if created:
×
1310
                click.echo("DynamoDB table created: %s" % table_name)
×
1311
            else:
1312
                click.echo("DynamoDB table exists: %s" % table_name)
×
1313
                provisioned_throughput = response_table["Table"]["ProvisionedThroughput"]
×
1314
                if (
×
1315
                    provisioned_throughput["ReadCapacityUnits"] != read_capacity
1316
                    or provisioned_throughput["WriteCapacityUnits"] != write_capacity
1317
                ):
1318
                    click.echo(
×
1319
                        click.style(
1320
                            "\nWarning! Existing DynamoDB table ({}) does not match configured capacity.\n".format(table_name),
1321
                            fg="red",
1322
                        )
1323
                    )
1324

1325
    def unschedule(self):
5✔
1326
        """
1327
        Given a a list of scheduled functions,
1328
        tear down their regular execution.
1329
        """
1330

1331
        # Run even if events are not defined to remove previously existing ones (thus default to []).
1332
        events = self.stage_config.get("events", [])
5✔
1333

1334
        if not isinstance(events, list):  # pragma: no cover
1335
            print("Events must be supplied as a list.")
1336
            return
1337

1338
        function_arn = None
5✔
1339
        try:
5✔
1340
            function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name)
5✔
1341
            function_arn = function_response["Configuration"]["FunctionArn"]
5✔
1342
        except botocore.exceptions.ClientError:  # pragma: no cover
1343
            raise ClickException(
1344
                "Function does not exist, you should deploy first. Ex: zappa deploy {}. "
1345
                "Proceeding to unschedule CloudWatch based events.".format(self.api_stage)
1346
            )
1347

1348
        print("Unscheduling..")
5✔
1349
        self.zappa.unschedule_events(
5✔
1350
            lambda_name=self.lambda_name,
1351
            lambda_arn=function_arn,
1352
            events=events,
1353
        )
1354

1355
        # Remove async task SNS
1356
        if self.stage_config.get("async_source", None) == "sns" and self.stage_config.get("async_resources", True):
5✔
1357
            removed_arns = self.zappa.remove_async_sns_topic(self.lambda_name)
×
1358
            click.echo("SNS Topic removed: %s" % ", ".join(removed_arns))
×
1359

1360
    def invoke(self, function_name, raw_python=False, command=None, no_color=False):
5✔
1361
        """
1362
        Invoke a remote function.
1363
        """
1364

1365
        # There are three likely scenarios for 'command' here:
1366
        #   command, which is a modular function path
1367
        #   raw_command, which is a string of python to execute directly
1368
        #   manage, which is a Django-specific management command invocation
1369
        key = command if command is not None else "command"
×
1370
        if raw_python:
×
1371
            command = {"raw_command": function_name}
×
1372
        else:
1373
            command = {key: function_name}
×
1374

1375
        # Can't use hjson
1376
        import json as json
×
1377

1378
        response = self.zappa.invoke_lambda_function(
×
1379
            self.lambda_name,
1380
            json.dumps(command),
1381
            invocation_type="RequestResponse",
1382
        )
1383

1384
        print(self.format_lambda_response(response, not no_color))
×
1385

1386
        # For a successful request FunctionError is not in response.
1387
        # https://github.com/Miserlou/Zappa/pull/1254/
1388
        if "FunctionError" in response:
×
1389
            raise ClickException("{} error occurred while invoking command.".format(response["FunctionError"]))
×
1390

1391
    def format_lambda_response(self, response, colorize=True):
5✔
1392
        if "LogResult" in response:
5✔
1393
            logresult_bytes = base64.b64decode(response["LogResult"])
5✔
1394
            try:
5✔
1395
                decoded = logresult_bytes.decode()
5✔
1396
            except UnicodeDecodeError:
5✔
1397
                return logresult_bytes
5✔
1398
            else:
1399
                if colorize and sys.stdout.isatty():
5✔
1400
                    formatted = self.format_invoke_command(decoded)
5✔
1401
                    return self.colorize_invoke_command(formatted)
5✔
1402
                else:
1403
                    return decoded
5✔
1404
        else:
1405
            return response
5✔
1406

1407
    def format_invoke_command(self, string):
5✔
1408
        """
1409
        Formats correctly the string output from the invoke() method,
1410
        replacing line breaks and tabs when necessary.
1411
        """
1412

1413
        string = string.replace("\\n", "\n")
5✔
1414

1415
        formated_response = ""
5✔
1416
        for line in string.splitlines():
5✔
1417
            if line.startswith("REPORT"):
5✔
1418
                line = line.replace("\t", "\n")
5✔
1419
            if line.startswith("[DEBUG]"):
5✔
1420
                line = line.replace("\t", " ")
5✔
1421
            formated_response += line + "\n"
5✔
1422
        formated_response = formated_response.replace("\n\n", "\n")
5✔
1423

1424
        return formated_response
5✔
1425

1426
    def colorize_invoke_command(self, string):
5✔
1427
        """
1428
        Apply various heuristics to return a colorized version the invoke
1429
        command string. If these fail, simply return the string in plaintext.
1430
        Inspired by colorize_log_entry().
1431
        """
1432

1433
        final_string = string
5✔
1434

1435
        try:
5✔
1436
            # Line headers
1437
            try:
5✔
1438
                for token in ["START", "END", "REPORT", "[DEBUG]"]:
5✔
1439
                    if token in final_string:
5✔
1440
                        format_string = "[{}]"
5✔
1441
                        # match whole words only
1442
                        pattern = r"\b{}\b"
5✔
1443
                        if token == "[DEBUG]":
5✔
1444
                            format_string = "{}"
5✔
1445
                            pattern = re.escape(token)
5✔
1446
                        repl = click.style(format_string.format(token), bold=True, fg="cyan")
5✔
1447
                        final_string = re.sub(pattern.format(token), repl, final_string)
5✔
1448
            except Exception:  # pragma: no cover
1449
                pass
1450

1451
            # Green bold Tokens
1452
            try:
5✔
1453
                for token in [
5✔
1454
                    "Zappa Event:",
1455
                    "RequestId:",
1456
                    "Version:",
1457
                    "Duration:",
1458
                    "Billed",
1459
                    "Memory Size:",
1460
                    "Max Memory Used:",
1461
                ]:
1462
                    if token in final_string:
5✔
1463
                        final_string = final_string.replace(token, click.style(token, bold=True, fg="green"))
5✔
1464
            except Exception:  # pragma: no cover
1465
                pass
1466

1467
            # UUIDs
1468
            for token in final_string.replace("\t", " ").split(" "):
5✔
1469
                try:
5✔
1470
                    if token.count("-") == 4 and token.replace("-", "").isalnum():
5✔
1471
                        final_string = final_string.replace(token, click.style(token, fg="magenta"))
5✔
1472
                except Exception:  # pragma: no cover
1473
                    pass
1474

1475
            return final_string
5✔
1476
        except Exception:
×
1477
            return string
×
1478

1479
    def status(self, return_json=False):
5✔
1480
        """
1481
        Describe the status of the current deployment.
1482
        """
1483

1484
        def tabular_print(title, value):
5✔
1485
            """
1486
            Convenience function for priting formatted table items.
1487
            """
1488
            click.echo("%-*s%s" % (32, click.style("\t" + title, fg="green") + ":", str(value)))
5✔
1489
            return
5✔
1490

1491
        # Lambda Env Details
1492
        lambda_versions = self.zappa.get_lambda_function_versions(self.lambda_name)
5✔
1493

1494
        if not lambda_versions:
5✔
1495
            raise ClickException(
×
1496
                click.style(
1497
                    "No Lambda %s detected in %s - have you deployed yet?" % (self.lambda_name, self.zappa.aws_region),
1498
                    fg="red",
1499
                )
1500
            )
1501

1502
        status_dict = collections.OrderedDict()
5✔
1503
        status_dict["Lambda Versions"] = len(lambda_versions)
5✔
1504
        function_response = self.zappa.lambda_client.get_function(FunctionName=self.lambda_name)
5✔
1505
        conf = function_response["Configuration"]
5✔
1506
        self.lambda_arn = conf["FunctionArn"]
5✔
1507
        status_dict["Lambda Name"] = self.lambda_name
5✔
1508
        status_dict["Lambda ARN"] = self.lambda_arn
5✔
1509
        status_dict["Lambda Role ARN"] = conf["Role"]
5✔
1510
        status_dict["Lambda Code Size"] = conf["CodeSize"]
5✔
1511
        status_dict["Lambda Version"] = conf["Version"]
5✔
1512
        status_dict["Lambda Last Modified"] = conf["LastModified"]
5✔
1513
        status_dict["Lambda Memory Size"] = conf["MemorySize"]
5✔
1514
        status_dict["Lambda Timeout"] = conf["Timeout"]
5✔
1515
        # Handler & Runtime won't be present for lambda Docker deployments
1516
        # https://github.com/Miserlou/Zappa/issues/2188
1517
        status_dict["Lambda Handler"] = conf.get("Handler", "")
5✔
1518
        status_dict["Lambda Runtime"] = conf.get("Runtime", "")
5✔
1519
        if "VpcConfig" in conf.keys():
5✔
1520
            status_dict["Lambda VPC ID"] = conf.get("VpcConfig", {}).get("VpcId", "Not assigned")
×
1521
        else:
1522
            status_dict["Lambda VPC ID"] = None
5✔
1523

1524
        # Calculated statistics
1525
        try:
5✔
1526
            function_invocations = self.zappa.cloudwatch.get_metric_statistics(
5✔
1527
                Namespace="AWS/Lambda",
1528
                MetricName="Invocations",
1529
                StartTime=datetime.utcnow() - timedelta(days=1),
1530
                EndTime=datetime.utcnow(),
1531
                Period=1440,
1532
                Statistics=["Sum"],
1533
                Dimensions=[{"Name": "FunctionName", "Value": "{}".format(self.lambda_name)}],
1534
            )["Datapoints"][0]["Sum"]
1535
        except Exception:
×
1536
            function_invocations = 0
×
1537
        try:
5✔
1538
            function_errors = self.zappa.cloudwatch.get_metric_statistics(
5✔
1539
                Namespace="AWS/Lambda",
1540
                MetricName="Errors",
1541
                StartTime=datetime.utcnow() - timedelta(days=1),
1542
                EndTime=datetime.utcnow(),
1543
                Period=1440,
1544
                Statistics=["Sum"],
1545
                Dimensions=[{"Name": "FunctionName", "Value": "{}".format(self.lambda_name)}],
1546
            )["Datapoints"][0]["Sum"]
1547
        except Exception:
×
1548
            function_errors = 0
×
1549

1550
        try:
5✔
1551
            error_rate = "{0:.2f}%".format(function_errors / function_invocations * 100)
5✔
1552
        except Exception:
×
1553
            error_rate = "Error calculating"
×
1554
        status_dict["Invocations (24h)"] = int(function_invocations)
5✔
1555
        status_dict["Errors (24h)"] = int(function_errors)
5✔
1556
        status_dict["Error Rate (24h)"] = error_rate
5✔
1557

1558
        # URLs
1559
        if self.use_apigateway:
5✔
1560
            api_url = self.zappa.get_api_url(self.lambda_name, self.api_stage)
5✔
1561

1562
            status_dict["API Gateway URL"] = api_url
5✔
1563

1564
            # Api Keys
1565
            api_id = self.zappa.get_api_id(self.lambda_name)
5✔
1566
            for api_key in self.zappa.get_api_keys(api_id, self.api_stage):
5✔
1567
                status_dict["API Gateway x-api-key"] = api_key
5✔
1568

1569
            # There literally isn't a better way to do this.
1570
            # AWS provides no way to tie a APIGW domain name to its Lambda function.
1571
            domain_url = self.stage_config.get("domain", None)
5✔
1572
            base_path = self.stage_config.get("base_path", None)
5✔
1573
            if domain_url:
5✔
1574
                status_dict["Domain URL"] = "https://" + domain_url
×
1575
                if base_path:
×
1576
                    status_dict["Domain URL"] += "/" + base_path
×
1577
            else:
1578
                status_dict["Domain URL"] = "None Supplied"
5✔
1579

1580
        # Scheduled Events
1581
        event_rules = self.zappa.get_event_rules_for_lambda(lambda_arn=self.lambda_arn)
5✔
1582
        status_dict["Num. Event Rules"] = len(event_rules)
5✔
1583
        if len(event_rules) > 0:
5✔
1584
            status_dict["Events"] = []
5✔
1585
        for rule in event_rules:
5✔
1586
            event_dict = {}
5✔
1587
            rule_name = rule["Name"]
5✔
1588
            event_dict["Event Rule Name"] = rule_name
5✔
1589
            event_dict["Event Rule Schedule"] = rule.get("ScheduleExpression", None)
5✔
1590
            event_dict["Event Rule State"] = rule.get("State", None).title()
5✔
1591
            event_dict["Event Rule ARN"] = rule.get("Arn", None)
5✔
1592
            status_dict["Events"].append(event_dict)
5✔
1593

1594
        if return_json:
5✔
1595
            # Putting the status in machine readable format
1596
            # https://github.com/Miserlou/Zappa/issues/407
1597
            print(json.dumpsJSON(status_dict))
×
1598
        else:
1599
            click.echo("Status for " + click.style(self.lambda_name, bold=True) + ": ")
5✔
1600
            for k, v in status_dict.items():
5✔
1601
                if k == "Events":
5✔
1602
                    # Events are a list of dicts
1603
                    for event in v:
5✔
1604
                        for item_k, item_v in event.items():
5✔
1605
                            tabular_print(item_k, item_v)
5✔
1606
                else:
1607
                    tabular_print(k, v)
5✔
1608

1609
        # TODO: S3/SQS/etc. type events?
1610

1611
        return True
5✔
1612

1613
    def check_stage_name(self, stage_name):
5✔
1614
        """
1615
        Make sure the stage name matches the AWS-allowed pattern
1616
        (calls to apigateway_client.create_deployment, will fail with error
1617
        message "ClientError: An error occurred (BadRequestException) when
1618
        calling the CreateDeployment operation: Stage name only allows
1619
        a-zA-Z0-9_" if the pattern does not match)
1620
        """
1621
        if not self.use_apigateway:
5✔
1622
            return True
×
1623
        if self.stage_name_env_pattern.match(stage_name):
5✔
1624
            return True
×
1625
        raise ValueError("API stage names must match a-zA-Z0-9_ ; '{0!s}' does not.".format(stage_name))
5✔
1626

1627
    def check_environment(self, environment):
5✔
1628
        """
1629
        Make sure the environment contains only strings
1630
        (since putenv needs a string)
1631
        """
1632

1633
        non_strings = []
5✔
1634
        for k, v in environment.items():
5✔
1635
            if not isinstance(v, str):
5✔
1636
                non_strings.append(k)
5✔
1637
        if non_strings:
5✔
1638
            raise ValueError("The following environment variables are not strings: {}".format(", ".join(non_strings)))
5✔
1639
        else:
1640
            return True
5✔
1641

1642
    def init(self, settings_file="zappa_settings.json"):
5✔
1643
        """
1644
        Initialize a new Zappa project by creating a new zappa_settings.json in a guided process.
1645
        This should probably be broken up into few separate componants once it's stable.
1646
        Testing these inputs requires monkeypatching with mock, which isn't pretty.
1647
        """
1648

1649
        # Make sure we're in a venv.
1650
        self.check_venv()
×
1651

1652
        # Ensure that we don't already have a zappa_settings file.
1653
        if os.path.isfile(settings_file):
×
1654
            raise ClickException(
×
1655
                "This project already has a " + click.style("{0!s} file".format(settings_file), fg="red", bold=True) + "!"
1656
            )
1657

1658
        # Explain system.
1659
        click.echo(
×
1660
            click.style(
1661
                """\n███████╗ █████╗ ██████╗ ██████╗  █████╗
1662
╚══███╔╝██╔══██╗██╔══██╗██╔══██╗██╔══██╗
1663
  ███╔╝ ███████║██████╔╝██████╔╝███████║
1664
 ███╔╝  ██╔══██║██╔═══╝ ██╔═══╝ ██╔══██║
1665
███████╗██║  ██║██║     ██║     ██║  ██║
1666
╚══════╝╚═╝  ╚═╝╚═╝     ╚═╝     ╚═╝  ╚═╝\n""",
1667
                fg="green",
1668
                bold=True,
1669
            )
1670
        )
1671

1672
        click.echo(
×
1673
            click.style("Welcome to ", bold=True) + click.style("Zappa", fg="green", bold=True) + click.style("!\n", bold=True)
1674
        )
1675
        click.echo(
×
1676
            click.style("Zappa", bold=True) + " is a system for running server-less Python web applications"
1677
            " on AWS Lambda and AWS API Gateway."
1678
        )
1679
        click.echo("This `init` command will help you create and configure your new Zappa deployment.")
×
1680
        click.echo("Let's get started!\n")
×
1681

1682
        # Create Env
1683
        while True:
1684
            click.echo(
×
1685
                "Your Zappa configuration can support multiple production stages, like '"
1686
                + click.style("dev", bold=True)
1687
                + "', '"
1688
                + click.style("staging", bold=True)
1689
                + "', and '"
1690
                + click.style("production", bold=True)
1691
                + "'."
1692
            )
1693
            env = input("What do you want to call this environment (default 'dev'): ") or "dev"
×
1694
            try:
×
1695
                self.check_stage_name(env)
×
1696
                break
×
1697
            except ValueError:
×
1698
                click.echo(click.style("Stage names must match a-zA-Z0-9_", fg="red"))
×
1699

1700
        # Detect AWS profiles and regions
1701
        # If anyone knows a more straightforward way to easily detect and
1702
        # parse AWS profiles I'm happy to change this, feels like a hack
1703
        session = botocore.session.Session()
×
1704
        config = session.full_config
×
1705
        profiles = config.get("profiles", {})
×
1706
        profile_names = list(profiles.keys())
×
1707

1708
        click.echo(
×
1709
            "\nAWS Lambda and API Gateway are only available in certain regions. "
1710
            "Let's check to make sure you have a profile set up in one that will work."
1711
        )
1712

1713
        if not profile_names:
×
1714
            profile_name, profile = None, None
×
1715
            click.echo(
×
1716
                "We couldn't find an AWS profile to use. "
1717
                "Before using Zappa, you'll need to set one up. See here for more info: {}".format(
1718
                    click.style(BOTO3_CONFIG_DOCS_URL, fg="blue", underline=True)
1719
                )
1720
            )
1721
        elif len(profile_names) == 1:
×
1722
            profile_name = profile_names[0]
×
1723
            profile = profiles[profile_name]
×
1724
            click.echo("Okay, using profile {}!".format(click.style(profile_name, bold=True)))
×
1725
        else:
1726
            if "default" in profile_names:
×
1727
                default_profile = [p for p in profile_names if p == "default"][0]
×
1728
            else:
1729
                default_profile = profile_names[0]
×
1730

1731
            while True:
1732
                profile_name = (
×
1733
                    input(
1734
                        "We found the following profiles: {}, and {}. "
1735
                        "Which would you like us to use? (default '{}'): ".format(
1736
                            ", ".join(profile_names[:-1]),
1737
                            profile_names[-1],
1738
                            default_profile,
1739
                        )
1740
                    )
1741
                    or default_profile
1742
                )
1743
                if profile_name in profiles:
×
1744
                    profile = profiles[profile_name]
×
1745
                    break
×
1746
                else:
1747
                    click.echo("Please enter a valid name for your AWS profile.")
×
1748

1749
        profile_region = profile.get("region") if profile else None
×
1750

1751
        # Create Bucket
1752
        click.echo(
×
1753
            "\nYour Zappa deployments will need to be uploaded to a " + click.style("private S3 bucket", bold=True) + "."
1754
        )
1755
        click.echo("If you don't have a bucket yet, we'll create one for you too.")
×
1756
        default_bucket = "zappa-" + "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(9))
×
1757
        while True:
1758
            bucket = input("What do you want to call your bucket? (default '%s'): " % default_bucket) or default_bucket
×
1759

1760
            if is_valid_bucket_name(bucket):
×
1761
                break
×
1762

1763
            click.echo(click.style("Invalid bucket name!", bold=True))
×
1764
            click.echo("S3 buckets must be named according to the following rules:")
×
1765
            click.echo(
×
1766
                """* Bucket names must be unique across all existing bucket names in Amazon S3.
1767
* Bucket names must comply with DNS naming conventions.
1768
* Bucket names must be at least 3 and no more than 63 characters long.
1769
* Bucket names must not contain uppercase characters or underscores.
1770
* Bucket names must start with a lowercase letter or number.
1771
* Bucket names must be a series of one or more labels. Adjacent labels are separated
1772
  by a single period (.). Bucket names can contain lowercase letters, numbers, and
1773
  hyphens. Each label must start and end with a lowercase letter or a number.
1774
* Bucket names must not be formatted as an IP address (for example, 192.168.5.4).
1775
* When you use virtual hosted–style buckets with Secure Sockets Layer (SSL), the SSL
1776
  wildcard certificate only matches buckets that don't contain periods. To work around
1777
  this, use HTTP or write your own certificate verification logic. We recommend that
1778
  you do not use periods (".") in bucket names when using virtual hosted–style buckets.
1779
"""
1780
            )
1781

1782
        # Detect Django/Flask
1783
        try:  # pragma: no cover
1784
            import django  # noqa: F401
1785

1786
            has_django = True
1787
        except ImportError:
×
1788
            has_django = False
×
1789

1790
        try:  # pragma: no cover
1791
            import flask  # noqa: F401
1792

1793
            has_flask = True
1794
        except ImportError:
×
1795
            has_flask = False
×
1796

1797
        print("")
×
1798
        # App-specific
1799
        if has_django:  # pragma: no cover
1800
            click.echo("It looks like this is a " + click.style("Django", bold=True) + " application!")
1801
            click.echo("What is the " + click.style("module path", bold=True) + " to your projects's Django settings?")
1802
            django_settings = None
1803

1804
            matches = detect_django_settings()
1805
            while django_settings in [None, ""]:
1806
                if matches:
1807
                    click.echo(
1808
                        "We discovered: "
1809
                        + click.style(
1810
                            ", ".join("{}".format(i) for v, i in enumerate(matches)),
1811
                            bold=True,
1812
                        )
1813
                    )
1814
                    django_settings = input("Where are your project's settings? (default '%s'): " % matches[0]) or matches[0]
1815
                else:
1816
                    click.echo("(This will likely be something like 'your_project.settings')")
1817
                    django_settings = input("Where are your project's settings?: ")
1818
            django_settings = django_settings.replace("'", "")
1819
            django_settings = django_settings.replace('"', "")
1820
        else:
1821
            matches = None
×
1822
            if has_flask:
×
1823
                click.echo("It looks like this is a " + click.style("Flask", bold=True) + " application.")
×
1824
                matches = detect_flask_apps()
×
1825
            click.echo("What's the " + click.style("modular path", bold=True) + " to your app's function?")
×
1826
            click.echo("This will likely be something like 'your_module.app'.")
×
1827
            app_function = None
×
1828
            while app_function in [None, ""]:
×
1829
                if matches:
×
1830
                    click.echo(
×
1831
                        "We discovered: "
1832
                        + click.style(
1833
                            ", ".join("{}".format(i) for v, i in enumerate(matches)),
1834
                            bold=True,
1835
                        )
1836
                    )
1837
                    app_function = input("Where is your app's function? (default '%s'): " % matches[0]) or matches[0]
×
1838
                else:
1839
                    app_function = input("Where is your app's function?: ")
×
1840
            app_function = app_function.replace("'", "")
×
1841
            app_function = app_function.replace('"', "")
×
1842

1843
        # TODO: Create VPC?
1844
        # Memory size? Time limit?
1845
        # Domain? LE keys? Region?
1846
        # 'Advanced Settings' mode?
1847

1848
        # Globalize
1849
        click.echo(
×
1850
            "\nYou can optionally deploy to "
1851
            + click.style("all available regions", bold=True)
1852
            + " in order to provide fast global service."
1853
        )
1854
        click.echo("If you are using Zappa for the first time, you probably don't want to do this!")
×
1855
        global_deployment = False
×
1856
        while True:
1857
            global_type = input(
×
1858
                "Would you like to deploy this application "
1859
                + click.style("globally", bold=True)
1860
                + "? (default 'n') [y/n/(p)rimary]: "
1861
            )
1862
            if not global_type:
×
1863
                break
×
1864
            if global_type.lower() in ["y", "yes", "p", "primary"]:
×
1865
                global_deployment = True
×
1866
                break
×
1867
            if global_type.lower() in ["n", "no"]:
×
1868
                global_deployment = False
×
1869
                break
×
1870

1871
        # The given environment name
1872
        zappa_settings = {
×
1873
            env: {
1874
                "profile_name": profile_name,
1875
                "s3_bucket": bucket,
1876
                "runtime": get_venv_from_python_version(),
1877
                "project_name": self.get_project_name(),
1878
                "exclude": ["boto3", "dateutil", "botocore", "s3transfer", "concurrent"],
1879
            }
1880
        }
1881

1882
        if profile_region:
×
1883
            zappa_settings[env]["aws_region"] = profile_region
×
1884

1885
        if has_django:
×
1886
            zappa_settings[env]["django_settings"] = django_settings
×
1887
        else:
1888
            zappa_settings[env]["app_function"] = app_function
×
1889

1890
        # Global Region Deployment
1891
        if global_deployment:
×
1892
            additional_regions = [r for r in API_GATEWAY_REGIONS if r != profile_region]
×
1893
            # Create additional stages
1894
            if global_type.lower() in ["p", "primary"]:
×
1895
                additional_regions = [r for r in additional_regions if "-1" in r]
×
1896

1897
            for region in additional_regions:
×
1898
                env_name = env + "_" + region.replace("-", "_")
×
1899
                g_env = {env_name: {"extends": env, "aws_region": region}}
×
1900
                zappa_settings.update(g_env)
×
1901

1902
        import json as json  # hjson is fine for loading, not fine for writing.
×
1903

1904
        zappa_settings_json = json.dumps(zappa_settings, sort_keys=True, indent=4)
×
1905

1906
        click.echo("\nOkay, here's your " + click.style("zappa_settings.json", bold=True) + ":\n")
×
1907
        click.echo(click.style(zappa_settings_json, fg="yellow", bold=False))
×
1908

1909
        confirm = input("\nDoes this look " + click.style("okay", bold=True, fg="green") + "? (default 'y') [y/n]: ") or "yes"
×
1910
        if confirm[0] not in ["y", "Y", "yes", "YES"]:
×
1911
            click.echo("" + click.style("Sorry", bold=True, fg="red") + " to hear that! Please init again.")
×
1912
            return
×
1913

1914
        # Write
1915
        with open("zappa_settings.json", "w") as zappa_settings_file:
×
1916
            zappa_settings_file.write(zappa_settings_json)
×
1917

1918
        if global_deployment:
×
1919
            click.echo(
×
1920
                "\n"
1921
                + click.style("Done", bold=True)
1922
                + "! You can also "
1923
                + click.style("deploy all", bold=True)
1924
                + " by executing:\n"
1925
            )
1926
            click.echo(click.style("\t$ zappa deploy --all", bold=True))
×
1927

1928
            click.echo("\nAfter that, you can " + click.style("update", bold=True) + " your application code with:\n")
×
1929
            click.echo(click.style("\t$ zappa update --all", bold=True))
×
1930
        else:
1931
            click.echo(
×
1932
                "\n"
1933
                + click.style("Done", bold=True)
1934
                + "! Now you can "
1935
                + click.style("deploy", bold=True)
1936
                + " your Zappa application by executing:\n"
1937
            )
1938
            click.echo(click.style("\t$ zappa deploy %s" % env, bold=True))
×
1939

1940
            click.echo("\nAfter that, you can " + click.style("update", bold=True) + " your application code with:\n")
×
1941
            click.echo(click.style("\t$ zappa update %s" % env, bold=True))
×
1942

1943
        click.echo(
×
1944
            "\nTo learn more, check out our project page on "
1945
            + click.style("GitHub", bold=True)
1946
            + " here: "
1947
            + click.style("https://github.com/Zappa/Zappa", fg="cyan", bold=True)
1948
        )
1949
        click.echo(
×
1950
            "and stop by our "
1951
            + click.style("Slack", bold=True)
1952
            + " channel here: "
1953
            + click.style("https://zappateam.slack.com", fg="cyan", bold=True)
1954
        )
1955
        click.echo("\nEnjoy!,")
×
1956
        click.echo(" ~ Team " + click.style("Zappa", bold=True) + "!")
×
1957

1958
        return
×
1959

1960
    def certify(self, no_confirm=True, manual=False):
5✔
1961
        """
1962
        Register or update a domain certificate for this env.
1963
        """
1964

1965
        if not self.domain:
5✔
1966
            raise ClickException(
×
1967
                "Can't certify a domain without " + click.style("domain", fg="red", bold=True) + " configured!"
1968
            )
1969

1970
        if not no_confirm:  # pragma: no cover
1971
            confirm = input("Are you sure you want to certify? [y/n] ")
1972
            if confirm != "y":
1973
                return
1974

1975
        # Make sure this isn't already deployed.
1976
        deployed_versions = self.zappa.get_lambda_function_versions(self.lambda_name)
5✔
1977
        if len(deployed_versions) == 0:
5✔
1978
            raise ClickException(
5✔
1979
                "This application "
1980
                + click.style("isn't deployed yet", fg="red")
1981
                + " - did you mean to call "
1982
                + click.style("deploy", bold=True)
1983
                + "?"
1984
            )
1985

1986
        account_key_location = self.stage_config.get("lets_encrypt_key", None)
5✔
1987
        cert_location = self.stage_config.get("certificate", None)
5✔
1988
        cert_key_location = self.stage_config.get("certificate_key", None)
5✔
1989
        cert_chain_location = self.stage_config.get("certificate_chain", None)
5✔
1990
        cert_arn = self.stage_config.get("certificate_arn", None)
5✔
1991
        base_path = self.stage_config.get("base_path", None)
5✔
1992

1993
        # These are sensitive
1994
        certificate_body = None
5✔
1995
        certificate_private_key = None
5✔
1996
        certificate_chain = None
5✔
1997

1998
        # Prepare for custom Let's Encrypt
1999
        if not cert_location and not cert_arn:
5✔
2000
            if not account_key_location:
5✔
2001
                raise ClickException(
5✔
2002
                    "Can't certify a domain without "
2003
                    + click.style("lets_encrypt_key", fg="red", bold=True)
2004
                    + " or "
2005
                    + click.style("certificate", fg="red", bold=True)
2006
                    + " or "
2007
                    + click.style("certificate_arn", fg="red", bold=True)
2008
                    + " configured!"
2009
                )
2010

2011
            # Get install account_key to /tmp/account_key.pem
2012
            from .letsencrypt import gettempdir
×
2013

2014
            if account_key_location.startswith("s3://"):
×
2015
                bucket, key_name = parse_s3_url(account_key_location)
×
2016
                self.zappa.s3_client.download_file(bucket, key_name, os.path.join(gettempdir(), "account.key"))
×
2017
            else:
2018
                from shutil import copyfile
×
2019

2020
                copyfile(account_key_location, os.path.join(gettempdir(), "account.key"))
×
2021

2022
        # Prepare for Custom SSL
2023
        elif not account_key_location and not cert_arn:
5✔
2024
            if not cert_location or not cert_key_location or not cert_chain_location:
5✔
2025
                raise ClickException(
5✔
2026
                    "Can't certify a domain without "
2027
                    + click.style(
2028
                        "certificate, certificate_key and certificate_chain",
2029
                        fg="red",
2030
                        bold=True,
2031
                    )
2032
                    + " configured!"
2033
                )
2034

2035
            # Read the supplied certificates.
2036
            with open(cert_location) as f:
5✔
2037
                certificate_body = f.read()
5✔
2038

2039
            with open(cert_key_location) as f:
5✔
2040
                certificate_private_key = f.read()
5✔
2041

2042
            with open(cert_chain_location) as f:
5✔
2043
                certificate_chain = f.read()
5✔
2044

2045
        click.echo("Certifying domain " + click.style(self.domain, fg="green", bold=True) + "..")
5✔
2046

2047
        # Get cert and update domain.
2048

2049
        # Let's Encrypt
2050
        if not cert_location and not cert_arn:
5✔
2051
            from .letsencrypt import get_cert_and_update_domain
×
2052

2053
            cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual)
×
2054

2055
        # Custom SSL / ACM
2056
        else:
2057
            route53 = self.stage_config.get("route53_enabled", True)
5✔
2058
            if not self.zappa.get_domain_name(self.domain, route53=route53):
5✔
2059
                dns_name = self.zappa.create_domain_name(
5✔
2060
                    domain_name=self.domain,
2061
                    certificate_name=self.domain + "-Zappa-Cert",
2062
                    certificate_body=certificate_body,
2063
                    certificate_private_key=certificate_private_key,
2064
                    certificate_chain=certificate_chain,
2065
                    certificate_arn=cert_arn,
2066
                    lambda_name=self.lambda_name,
2067
                    stage=self.api_stage,
2068
                    base_path=base_path,
2069
                )
2070
                if route53:
5✔
2071
                    self.zappa.update_route53_records(self.domain, dns_name)
5✔
2072
                print(
5✔
2073
                    "Created a new domain name with supplied certificate. "
2074
                    "Please note that it can take up to 40 minutes for this domain to be "
2075
                    "created and propagated through AWS, but it requires no further work on your part."
2076
                )
2077
            else:
2078
                self.zappa.update_domain_name(
5✔
2079
                    domain_name=self.domain,
2080
                    certificate_name=self.domain + "-Zappa-Cert",
2081
                    certificate_body=certificate_body,
2082
                    certificate_private_key=certificate_private_key,
2083
                    certificate_chain=certificate_chain,
2084
                    certificate_arn=cert_arn,
2085
                    lambda_name=self.lambda_name,
2086
                    stage=self.api_stage,
2087
                    route53=route53,
2088
                    base_path=base_path,
2089
                )
2090

2091
            cert_success = True
5✔
2092

2093
        if cert_success:
5✔
2094
            click.echo("Certificate " + click.style("updated", fg="green", bold=True) + "!")
5✔
2095
        else:
2096
            click.echo(click.style("Failed", fg="red", bold=True) + " to generate or install certificate! :(")
×
2097
            click.echo("\n==============\n")
×
2098
            shamelessly_promote()
×
2099

2100
    ##
2101
    # Shell
2102
    ##
2103
    def shell(self):
5✔
2104
        """
2105
        Spawn a debug shell.
2106
        """
2107
        click.echo(
×
2108
            click.style("NOTICE!", fg="yellow", bold=True)
2109
            + " This is a "
2110
            + click.style("local", fg="green", bold=True)
2111
            + " shell, inside a "
2112
            + click.style("Zappa", bold=True)
2113
            + " object!"
2114
        )
2115
        self.zappa.shell()
×
2116
        return
×
2117

2118
    ##
2119
    # Utility
2120
    ##
2121

2122
    def callback(self, position):
5✔
2123
        """
2124
        Allows the execution of custom code between creation of the zip file and deployment to AWS.
2125
        :return: None
2126
        """
2127

2128
        callbacks = self.stage_config.get("callbacks", {})
5✔
2129
        callback = callbacks.get(position)
5✔
2130

2131
        if callback:
5✔
2132
            (mod_path, cb_func_name) = callback.rsplit(".", 1)
5✔
2133

2134
            try:  # Prefer callback in working directory
5✔
2135
                if mod_path.count(".") >= 1:  # Callback function is nested in a folder
5✔
2136
                    (mod_folder_path, mod_name) = mod_path.rsplit(".", 1)
5✔
2137
                    mod_folder_path_fragments = mod_folder_path.split(".")
5✔
2138
                    working_dir = os.path.join(os.getcwd(), *mod_folder_path_fragments)
5✔
2139
                else:
2140
                    mod_name = mod_path
5✔
2141
                    working_dir = os.getcwd()
5✔
2142

2143
                working_dir_importer = pkgutil.get_importer(working_dir)
5✔
2144
                module_ = working_dir_importer.find_spec(mod_name).loader.load_module(mod_name)
5✔
2145

2146
            except (ImportError, AttributeError):
×
2147
                try:  # Callback func might be in virtualenv
×
2148
                    module_ = importlib.import_module(mod_path)
×
2149
                except ImportError:  # pragma: no cover
2150
                    raise ClickException(
2151
                        click.style("Failed ", fg="red")
2152
                        + "to "
2153
                        + click.style(
2154
                            "import {position} callback ".format(position=position),
2155
                            bold=True,
2156
                        )
2157
                        + 'module: "{mod_path}"'.format(mod_path=click.style(mod_path, bold=True))
2158
                    )
2159

2160
            if not hasattr(module_, cb_func_name):  # pragma: no cover
2161
                raise ClickException(
2162
                    click.style("Failed ", fg="red")
2163
                    + "to "
2164
                    + click.style("find {position} callback ".format(position=position), bold=True)
2165
                    + 'function: "{cb_func_name}" '.format(cb_func_name=click.style(cb_func_name, bold=True))
2166
                    + 'in module "{mod_path}"'.format(mod_path=mod_path)
2167
                )
2168

2169
            cb_func = getattr(module_, cb_func_name)
5✔
2170
            cb_func(self)  # Call the function passing self
5✔
2171

2172
    def check_for_update(self):
5✔
2173
        """
2174
        Print a warning if there's a new Zappa version available.
2175
        """
2176
        try:
5✔
2177
            version = pkg_resources.require("zappa")[0].version
5✔
2178
            updateable = check_new_version_available(version)
5✔
2179
            if updateable:
5✔
UNCOV
2180
                click.echo(
×
2181
                    click.style("Important!", fg="yellow", bold=True)
2182
                    + " A new version of "
2183
                    + click.style("Zappa", bold=True)
2184
                    + " is available!"
2185
                )
UNCOV
2186
                click.echo("Upgrade with: " + click.style("pip install zappa --upgrade", bold=True))
×
UNCOV
2187
                click.echo(
×
2188
                    "Visit the project page on GitHub to see the latest changes: "
2189
                    + click.style("https://github.com/Zappa/Zappa", bold=True)
2190
                )
2191
        except Exception as e:  # pragma: no cover
2192
            print(e)
2193
            return
2194

2195
    def load_settings(self, settings_file=None, session=None):
5✔
2196
        """
2197
        Load the local zappa_settings file.
2198
        An existing boto session can be supplied, though this is likely for testing purposes.
2199
        Returns the loaded Zappa object.
2200
        """
2201

2202
        # Ensure we're passed a valid settings file.
2203
        if not settings_file:
5✔
2204
            settings_file = self.get_json_or_yaml_settings()
×
2205
        if not os.path.isfile(settings_file):
5✔
2206
            raise ClickException("Please configure your zappa_settings file.")
×
2207

2208
        # Load up file
2209
        self.load_settings_file(settings_file)
5✔
2210

2211
        # Make sure that this stage is our settings
2212
        if self.api_stage not in self.zappa_settings.keys():
5✔
2213
            raise ClickException("Please define stage '{0!s}' in your Zappa settings.".format(self.api_stage))
×
2214

2215
        # We need a working title for this project. Use one if supplied, else cwd dirname.
2216
        if "project_name" in self.stage_config:  # pragma: no cover
2217
            # If the name is invalid, this will throw an exception with message up stack
2218
            self.project_name = validate_name(self.stage_config["project_name"])
2219
        else:
2220
            self.project_name = self.get_project_name()
5✔
2221

2222
        # The name of the actual AWS Lambda function, ex, 'helloworld-dev'
2223
        # Assume that we already have have validated the name beforehand.
2224
        # Related:  https://github.com/Miserlou/Zappa/pull/664
2225
        #           https://github.com/Miserlou/Zappa/issues/678
2226
        #           And various others from Slack.
2227
        self.lambda_name = slugify.slugify(self.project_name + "-" + self.api_stage)
5✔
2228

2229
        # Load stage-specific settings
2230
        self.s3_bucket_name = self.stage_config.get(
5✔
2231
            "s3_bucket",
2232
            "zappa-" + "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(9)),
2233
        )
2234
        self.vpc_config = self.stage_config.get("vpc_config", {})
5✔
2235
        self.memory_size = self.stage_config.get("memory_size", 512)
5✔
2236
        self.ephemeral_storage = self.stage_config.get("ephemeral_storage", {"Size": 512})
5✔
2237

2238
        # Validate ephemeral storage structure and size
2239
        if "Size" not in self.ephemeral_storage:
5✔
2240
            raise ClickException("Please provide a valid Size for ephemeral_storage in your Zappa settings.")
5✔
2241
        elif not 512 <= self.ephemeral_storage["Size"] <= 10240:
5✔
2242
            raise ClickException("Please provide a valid ephemeral_storage size between 512 - 10240 in your Zappa settings.")
5✔
2243

2244
        self.app_function = self.stage_config.get("app_function", None)
5✔
2245
        self.exception_handler = self.stage_config.get("exception_handler", None)
5✔
2246
        self.aws_region = self.stage_config.get("aws_region", None)
5✔
2247
        self.debug = self.stage_config.get("debug", True)
5✔
2248
        self.prebuild_script = self.stage_config.get("prebuild_script", None)
5✔
2249
        self.profile_name = self.stage_config.get("profile_name", None)
5✔
2250
        self.log_level = self.stage_config.get("log_level", "DEBUG")
5✔
2251
        self.domain = self.stage_config.get("domain", None)
5✔
2252
        self.base_path = self.stage_config.get("base_path", None)
5✔
2253
        self.timeout_seconds = self.stage_config.get("timeout_seconds", 30)
5✔
2254
        dead_letter_arn = self.stage_config.get("dead_letter_arn", "")
5✔
2255
        self.dead_letter_config = {"TargetArn": dead_letter_arn} if dead_letter_arn else {}
5✔
2256
        self.cognito = self.stage_config.get("cognito", None)
5✔
2257
        self.num_retained_versions = self.stage_config.get("num_retained_versions", None)
5✔
2258

2259
        # Check for valid values of num_retained_versions
2260
        if self.num_retained_versions is not None and type(self.num_retained_versions) is not int:
5✔
2261
            raise ClickException(
×
2262
                "Please supply either an integer or null for num_retained_versions in the zappa_settings.json. Found %s"
2263
                % type(self.num_retained_versions)
2264
            )
2265
        elif type(self.num_retained_versions) is int and self.num_retained_versions < 1:
5✔
2266
            raise ClickException("The value for num_retained_versions in the zappa_settings.json should be greater than 0.")
×
2267

2268
        # Provide legacy support for `use_apigateway`, now `apigateway_enabled`.
2269
        # https://github.com/Miserlou/Zappa/issues/490
2270
        # https://github.com/Miserlou/Zappa/issues/493
2271
        self.use_apigateway = self.stage_config.get("use_apigateway", True)
5✔
2272
        if self.use_apigateway:
5✔
2273
            self.use_apigateway = self.stage_config.get("apigateway_enabled", True)
5✔
2274
        self.apigateway_description = self.stage_config.get("apigateway_description", None)
5✔
2275

2276
        self.lambda_handler = self.stage_config.get("lambda_handler", "handler.lambda_handler")
5✔
2277
        # DEPRECATED. https://github.com/Miserlou/Zappa/issues/456
2278
        self.remote_env_bucket = self.stage_config.get("remote_env_bucket", None)
5✔
2279
        self.remote_env_file = self.stage_config.get("remote_env_file", None)
5✔
2280
        self.remote_env = self.stage_config.get("remote_env", None)
5✔
2281
        self.settings_file = self.stage_config.get("settings_file", None)
5✔
2282
        self.django_settings = self.stage_config.get("django_settings", None)
5✔
2283
        self.manage_roles = self.stage_config.get("manage_roles", True)
5✔
2284
        self.binary_support = self.stage_config.get("binary_support", True)
5✔
2285
        self.api_key_required = self.stage_config.get("api_key_required", False)
5✔
2286
        self.api_key = self.stage_config.get("api_key")
5✔
2287
        self.endpoint_configuration = self.stage_config.get("endpoint_configuration", None)
5✔
2288
        self.iam_authorization = self.stage_config.get("iam_authorization", False)
5✔
2289
        self.cors = self.stage_config.get("cors", False)
5✔
2290
        self.lambda_description = self.stage_config.get("lambda_description", "Zappa Deployment")
5✔
2291
        self.lambda_concurrency = self.stage_config.get("lambda_concurrency", None)
5✔
2292
        self.environment_variables = self.stage_config.get("environment_variables", {})
5✔
2293
        self.aws_environment_variables = self.stage_config.get("aws_environment_variables", {})
5✔
2294
        self.check_environment(self.environment_variables)
5✔
2295
        self.authorizer = self.stage_config.get("authorizer", {})
5✔
2296
        self.runtime = self.stage_config.get("runtime", get_runtime_from_python_version())
5✔
2297
        self.aws_kms_key_arn = self.stage_config.get("aws_kms_key_arn", "")
5✔
2298
        self.context_header_mappings = self.stage_config.get("context_header_mappings", {})
5✔
2299
        self.xray_tracing = self.stage_config.get("xray_tracing", False)
5✔
2300
        self.desired_role_arn = self.stage_config.get("role_arn")
5✔
2301
        self.layers = self.stage_config.get("layers", None)
5✔
2302
        self.additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", None)
5✔
2303

2304
        # check that BINARY_SUPPORT is True if additional_text_mimetypes is provided
2305
        if self.additional_text_mimetypes and not self.binary_support:
5✔
2306
            raise ClickException("zappa_settings.json has additional_text_mimetypes defined, but binary_support is False!")
5✔
2307

2308
        # Load ALB-related settings
2309
        self.use_alb = self.stage_config.get("alb_enabled", False)
5✔
2310
        self.alb_vpc_config = self.stage_config.get("alb_vpc_config", {})
5✔
2311

2312
        # Additional tags
2313
        self.tags = self.stage_config.get("tags", {})
5✔
2314

2315
        desired_role_name = self.lambda_name + "-ZappaLambdaExecutionRole"
5✔
2316
        self.zappa = Zappa(
5✔
2317
            boto_session=session,
2318
            profile_name=self.profile_name,
2319
            aws_region=self.aws_region,
2320
            load_credentials=self.load_credentials,
2321
            desired_role_name=desired_role_name,
2322
            desired_role_arn=self.desired_role_arn,
2323
            runtime=self.runtime,
2324
            tags=self.tags,
2325
            endpoint_urls=self.stage_config.get("aws_endpoint_urls", {}),
2326
            xray_tracing=self.xray_tracing,
2327
        )
2328

2329
        for setting in CUSTOM_SETTINGS:
5✔
2330
            if setting in self.stage_config:
5✔
2331
                setting_val = self.stage_config[setting]
5✔
2332
                # Read the policy file contents.
2333
                if setting.endswith("policy"):
5✔
2334
                    with open(setting_val, "r") as f:
×
2335
                        setting_val = f.read()
×
2336
                setattr(self.zappa, setting, setting_val)
5✔
2337

2338
        if self.app_function:
5✔
2339
            self.collision_warning(self.app_function)
5✔
2340
            if self.app_function[-3:] == ".py":
5✔
2341
                click.echo(
×
2342
                    click.style("Warning!", fg="red", bold=True)
2343
                    + " Your app_function is pointing to a "
2344
                    + click.style("file and not a function", bold=True)
2345
                    + "! It should probably be something like 'my_file.app', not 'my_file.py'!"
2346
                )
2347

2348
        return self.zappa
5✔
2349

2350
    def get_json_or_yaml_settings(self, settings_name="zappa_settings"):
5✔
2351
        """
2352
        Return zappa_settings path as JSON or YAML (or TOML), as appropriate.
2353
        """
2354
        zs_json = settings_name + ".json"
5✔
2355
        zs_yml = settings_name + ".yml"
5✔
2356
        zs_yaml = settings_name + ".yaml"
5✔
2357
        zs_toml = settings_name + ".toml"
5✔
2358

2359
        # Must have at least one
2360
        if (
5✔
2361
            not os.path.isfile(zs_json)
2362
            and not os.path.isfile(zs_yml)
2363
            and not os.path.isfile(zs_yaml)
2364
            and not os.path.isfile(zs_toml)
2365
        ):
2366
            raise ClickException("Please configure a zappa_settings file or call `zappa init`.")
5✔
2367

2368
        # Prefer JSON
2369
        if os.path.isfile(zs_json):
5✔
2370
            settings_file = zs_json
5✔
2371
        elif os.path.isfile(zs_toml):
5✔
2372
            settings_file = zs_toml
5✔
2373
        elif os.path.isfile(zs_yml):
5✔
2374
            settings_file = zs_yml
5✔
2375
        else:
2376
            settings_file = zs_yaml
5✔
2377

2378
        return settings_file
5✔
2379

2380
    def load_settings_file(self, settings_file=None):
5✔
2381
        """
2382
        Load our settings file.
2383
        """
2384

2385
        if not settings_file:
5✔
2386
            settings_file = self.get_json_or_yaml_settings()
5✔
2387
        if not os.path.isfile(settings_file):
5✔
2388
            raise ClickException("Please configure your zappa_settings file or call `zappa init`.")
×
2389

2390
        path, ext = os.path.splitext(settings_file)
5✔
2391
        if ext == ".yml" or ext == ".yaml":
5✔
2392
            with open(settings_file) as yaml_file:
5✔
2393
                try:
5✔
2394
                    self.zappa_settings = yaml.safe_load(yaml_file)
5✔
2395
                except ValueError:  # pragma: no cover
2396
                    raise ValueError("Unable to load the Zappa settings YAML. It may be malformed.")
2397
        elif ext == ".toml":
5✔
2398
            with open(settings_file) as toml_file:
5✔
2399
                try:
5✔
2400
                    self.zappa_settings = toml.load(toml_file)
5✔
2401
                except ValueError:  # pragma: no cover
2402
                    raise ValueError("Unable to load the Zappa settings TOML. It may be malformed.")
2403
        else:
2404
            with open(settings_file) as json_file:
5✔
2405
                try:
5✔
2406
                    self.zappa_settings = json.load(json_file)
5✔
2407
                except ValueError:  # pragma: no cover
2408
                    raise ValueError("Unable to load the Zappa settings JSON. It may be malformed.")
2409

2410
    def create_package(self, output=None, use_zappa_release: Optional[str] = None):
5✔
2411
        """
2412
        Ensure that the package can be properly configured,
2413
        and then create it.
2414
        """
2415

2416
        # Create the Lambda zip package (includes project and virtualenvironment)
2417
        # Also define the path the handler file so it can be copied to the zip
2418
        # root for Lambda.
2419
        current_file = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))  # type: ignore[arg-type]
5✔
2420
        handler_file = os.sep.join(current_file.split(os.sep)[0:]) + os.sep + "handler.py"
5✔
2421

2422
        # Create the zip file(s)
2423
        if self.stage_config.get("slim_handler", False):
5✔
2424
            # Create two zips. One with the application and the other with just the handler.
2425
            # https://github.com/Miserlou/Zappa/issues/510
2426
            self.zip_path = self.zappa.create_lambda_zip(  # type: ignore[attr-defined]
5✔
2427
                prefix=self.lambda_name,
2428
                use_precompiled_packages=self.stage_config.get("use_precompiled_packages", True),
2429
                exclude=self.stage_config.get("exclude", []),
2430
                exclude_glob=self.stage_config.get("exclude_glob", []),
2431
                disable_progress=self.disable_progress,
2432
                archive_format="tarball",
2433
            )
2434

2435
            # Make sure the normal venv is not included in the handler's zip
2436
            exclude = self.stage_config.get("exclude", [])
5✔
2437
            cur_venv = self.zappa.get_current_venv()  # type: ignore[attr-defined]
5✔
2438
            exclude.append(cur_venv.split("/")[-1])
5✔
2439
            self.handler_path = self.zappa.create_lambda_zip(  # type: ignore[attr-defined]
5✔
2440
                prefix="handler_{0!s}".format(self.lambda_name),
2441
                venv=self.zappa.create_handler_venv(use_zappa_release=use_zappa_release),  # type: ignore[attr-defined]
2442
                handler_file=handler_file,
2443
                slim_handler=True,
2444
                exclude=exclude,
2445
                exclude_glob=self.stage_config.get("exclude_glob", []),
2446
                output=output,
2447
                disable_progress=self.disable_progress,
2448
            )
2449
        else:
2450
            exclude = self.stage_config.get("exclude", [])
5✔
2451

2452
            # Create a single zip that has the handler and application
2453
            self.zip_path = self.zappa.create_lambda_zip(  # type: ignore[attr-defined]
5✔
2454
                prefix=self.lambda_name,
2455
                handler_file=handler_file,
2456
                use_precompiled_packages=self.stage_config.get("use_precompiled_packages", True),
2457
                exclude=exclude,
2458
                exclude_glob=self.stage_config.get("exclude_glob", []),
2459
                output=output,
2460
                disable_progress=self.disable_progress,
2461
            )
2462

2463
            # Warn if this is too large for Lambda.
2464
            file_stats = os.stat(self.zip_path)
5✔
2465
            if file_stats.st_size > 52428800:  # pragma: no cover
2466
                print(
2467
                    "\n\nWarning: Application zip package is likely to be too large for AWS Lambda. "
2468
                    'Try setting "slim_handler" to true in your Zappa settings file.\n\n'
2469
                )
2470

2471
        # Throw custom settings into the zip that handles requests
2472
        if self.stage_config.get("slim_handler", False):
5✔
2473
            handler_zip = self.handler_path
5✔
2474
        else:
2475
            handler_zip = self.zip_path
5✔
2476

2477
        with zipfile.ZipFile(handler_zip, "a") as lambda_zip:  # type: ignore[call-overload]
5✔
2478
            settings_s = self.get_zappa_settings_string()
5✔
2479

2480
            # Copy our Django app into root of our package.
2481
            # It doesn't work otherwise.
2482
            if self.django_settings:
5✔
2483
                base = __file__.rsplit(os.sep, 1)[0]
×
2484
                django_py = "".join(os.path.join(base, "ext", "django_zappa.py"))
×
2485
                lambda_zip.write(django_py, "django_zappa_app.py")
×
2486

2487
            # Lambda requires a specific chmod
2488
            temp_settings = tempfile.NamedTemporaryFile(delete=False)
5✔
2489
            os.chmod(temp_settings.name, 0o644)
5✔
2490
            temp_settings.write(bytes(settings_s, "utf-8"))
5✔
2491
            temp_settings.close()
5✔
2492
            lambda_zip.write(temp_settings.name, "zappa_settings.py")
5✔
2493
            os.unlink(temp_settings.name)
5✔
2494

2495
    def get_zappa_settings_string(self):
5✔
2496
        settings_s = "# Generated by Zappa\n"
5✔
2497

2498
        if self.app_function:
5✔
2499
            if "." not in self.app_function:  # pragma: no cover
2500
                raise ClickException(
2501
                    "Your "
2502
                    + click.style("app_function", fg="red", bold=True)
2503
                    + " value is not a modular path."
2504
                    + " It needs to be in the format `"
2505
                    + click.style("your_module.your_app_object", bold=True)
2506
                    + "`."
2507
                )
2508
            app_module, app_function = self.app_function.rsplit(".", 1)
5✔
2509
            settings_s = settings_s + "APP_MODULE='{0!s}'\nAPP_FUNCTION='{1!s}'\n".format(app_module, app_function)
5✔
2510

2511
        if self.exception_handler:
5✔
2512
            settings_s += "EXCEPTION_HANDLER='{0!s}'\n".format(self.exception_handler)
×
2513
        else:
2514
            settings_s += "EXCEPTION_HANDLER=None\n"
5✔
2515

2516
        if self.debug:
5✔
2517
            settings_s = settings_s + "DEBUG=True\n"
5✔
2518
        else:
2519
            settings_s = settings_s + "DEBUG=False\n"
×
2520

2521
        settings_s = settings_s + "LOG_LEVEL='{0!s}'\n".format((self.log_level))
5✔
2522

2523
        if self.binary_support:
5✔
2524
            settings_s = settings_s + "BINARY_SUPPORT=True\n"
5✔
2525
        else:
2526
            settings_s = settings_s + "BINARY_SUPPORT=False\n"
×
2527

2528
        head_map_dict = {}
5✔
2529
        head_map_dict.update(dict(self.context_header_mappings))
5✔
2530
        settings_s = settings_s + "CONTEXT_HEADER_MAPPINGS={0}\n".format(head_map_dict)
5✔
2531

2532
        # If we're on a domain, we don't need to define the /<<env>> in
2533
        # the WSGI PATH
2534
        if self.domain:
5✔
2535
            settings_s = settings_s + "DOMAIN='{0!s}'\n".format((self.domain))
×
2536
        else:
2537
            settings_s = settings_s + "DOMAIN=None\n"
5✔
2538

2539
        if self.base_path:
5✔
2540
            settings_s = settings_s + "BASE_PATH='{0!s}'\n".format((self.base_path))
×
2541
        else:
2542
            settings_s = settings_s + "BASE_PATH=None\n"
5✔
2543

2544
        # Pass through remote config bucket and path
2545
        if self.remote_env:
5✔
2546
            settings_s = settings_s + "REMOTE_ENV='{0!s}'\n".format(self.remote_env)
5✔
2547
        # DEPRECATED. use remove_env instead
2548
        elif self.remote_env_bucket and self.remote_env_file:
5✔
2549
            settings_s = settings_s + "REMOTE_ENV='s3://{0!s}/{1!s}'\n".format(self.remote_env_bucket, self.remote_env_file)
5✔
2550

2551
        # Local envs
2552
        env_dict = {}
5✔
2553
        if self.aws_region:
5✔
2554
            env_dict["AWS_REGION"] = self.aws_region
×
2555
        env_dict.update(dict(self.environment_variables))
5✔
2556

2557
        # Environment variable keys must be ascii
2558
        # https://github.com/Miserlou/Zappa/issues/604
2559
        # https://github.com/Miserlou/Zappa/issues/998
2560
        try:
5✔
2561
            env_dict = dict((k.encode("ascii").decode("ascii"), v) for (k, v) in env_dict.items())
5✔
2562
        except Exception:
5✔
2563
            raise ValueError("Environment variable keys must be ascii.")
5✔
2564

2565
        settings_s = settings_s + "ENVIRONMENT_VARIABLES={0}\n".format(env_dict)
5✔
2566

2567
        # We can be environment-aware
2568
        settings_s = settings_s + "API_STAGE='{0!s}'\n".format((self.api_stage))
5✔
2569
        settings_s = settings_s + "PROJECT_NAME='{0!s}'\n".format((self.project_name))
5✔
2570

2571
        if self.settings_file:
5✔
2572
            settings_s = settings_s + "SETTINGS_FILE='{0!s}'\n".format((self.settings_file))
×
2573
        else:
2574
            settings_s = settings_s + "SETTINGS_FILE=None\n"
5✔
2575

2576
        if self.django_settings:
5✔
2577
            settings_s = settings_s + "DJANGO_SETTINGS='{0!s}'\n".format((self.django_settings))
×
2578
        else:
2579
            settings_s = settings_s + "DJANGO_SETTINGS=None\n"
5✔
2580

2581
        # If slim handler, path to project zip
2582
        if self.stage_config.get("slim_handler", False):
5✔
2583
            settings_s += "ARCHIVE_PATH='s3://{0!s}/{1!s}_{2!s}_current_project.tar.gz'\n".format(
5✔
2584
                self.s3_bucket_name, self.api_stage, self.project_name
2585
            )
2586

2587
            # since includes are for slim handler add the setting here by joining arbitrary list from zappa_settings file
2588
            # and tell the handler we are the slim_handler
2589
            # https://github.com/Miserlou/Zappa/issues/776
2590
            settings_s += "SLIM_HANDLER=True\n"
5✔
2591

2592
            include = self.stage_config.get("include", [])
5✔
2593
            if len(include) >= 1:
5✔
2594
                settings_s += "INCLUDE=" + str(include) + "\n"
×
2595

2596
        # AWS Events function mapping
2597
        event_mapping = {}
5✔
2598
        events = self.stage_config.get("events", [])
5✔
2599
        for event in events:
5✔
2600
            arn = event.get("event_source", {}).get("arn")
5✔
2601
            function = event.get("function")
5✔
2602
            if arn and function:
5✔
2603
                event_mapping[arn] = function
5✔
2604
        settings_s = settings_s + "AWS_EVENT_MAPPING={0!s}\n".format(event_mapping)
5✔
2605

2606
        # Map Lext bot events
2607
        bot_events = self.stage_config.get("bot_events", [])
5✔
2608
        bot_events_mapping = {}
5✔
2609
        for bot_event in bot_events:
5✔
2610
            event_source = bot_event.get("event_source", {})
×
2611
            intent = event_source.get("intent")
×
2612
            invocation_source = event_source.get("invocation_source")
×
2613
            function = bot_event.get("function")
×
2614
            if intent and invocation_source and function:
×
2615
                bot_events_mapping[str(intent) + ":" + str(invocation_source)] = function
×
2616

2617
        settings_s = settings_s + "AWS_BOT_EVENT_MAPPING={0!s}\n".format(bot_events_mapping)
5✔
2618

2619
        # Map cognito triggers
2620
        cognito_trigger_mapping = {}
5✔
2621
        cognito_config = self.stage_config.get("cognito", {})
5✔
2622
        triggers = cognito_config.get("triggers", [])
5✔
2623
        for trigger in triggers:
5✔
2624
            source = trigger.get("source")
5✔
2625
            function = trigger.get("function")
5✔
2626
            if source and function:
5✔
2627
                cognito_trigger_mapping[source] = function
5✔
2628
        settings_s = settings_s + "COGNITO_TRIGGER_MAPPING={0!s}\n".format(cognito_trigger_mapping)
5✔
2629

2630
        # Authorizer config
2631
        authorizer_function = self.authorizer.get("function", None)
5✔
2632
        if authorizer_function:
5✔
2633
            settings_s += "AUTHORIZER_FUNCTION='{0!s}'\n".format(authorizer_function)
×
2634

2635
        # async response
2636
        async_response_table = self.stage_config.get("async_response_table", "")
5✔
2637
        settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table)
5✔
2638

2639
        # additional_text_mimetypes
2640
        additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", [])
5✔
2641
        settings_s += f"ADDITIONAL_TEXT_MIMETYPES={additional_text_mimetypes}\n"
5✔
2642
        return settings_s
5✔
2643

2644
    def remove_local_zip(self):
5✔
2645
        """
2646
        Remove our local zip file.
2647
        """
2648

2649
        if self.stage_config.get("delete_local_zip", True):
5✔
2650
            try:
5✔
2651
                if os.path.isfile(self.zip_path):
5✔
2652
                    os.remove(self.zip_path)
5✔
2653
                if self.handler_path and os.path.isfile(self.handler_path):
5✔
2654
                    os.remove(self.handler_path)
5✔
2655
            except Exception:  # pragma: no cover
2656
                sys.exit(-1)
2657

2658
    def remove_uploaded_zip(self):
5✔
2659
        """
2660
        Remove the local and S3 zip file after uploading and updating.
2661
        """
2662

2663
        # Remove the uploaded zip from S3, because it is now registered..
2664
        if self.stage_config.get("delete_s3_zip", True):
5✔
2665
            self.zappa.remove_from_s3(self.zip_path, self.s3_bucket_name)
5✔
2666
            if self.stage_config.get("slim_handler", False):
5✔
2667
                # Need to keep the project zip as the slim handler uses it.
2668
                self.zappa.remove_from_s3(self.handler_path, self.s3_bucket_name)
×
2669

2670
    def on_exit(self):
5✔
2671
        """
2672
        Cleanup after the command finishes.
2673
        Always called: SystemExit, KeyboardInterrupt and any other Exception that occurs.
2674
        """
2675
        if self.zip_path:
5✔
2676
            # Only try to remove uploaded zip if we're running a command that has loaded credentials
2677
            if self.load_credentials:
5✔
2678
                self.remove_uploaded_zip()
5✔
2679

2680
            self.remove_local_zip()
5✔
2681

2682
    def print_logs(self, logs, colorize=True, http=False, non_http=False, force_colorize=None):
5✔
2683
        """
2684
        Parse, filter and print logs to the console.
2685
        """
2686

2687
        for log in logs:
5✔
2688
            timestamp = log["timestamp"]
5✔
2689
            message = log["message"]
5✔
2690
            if "START RequestId" in message:
5✔
2691
                continue
5✔
2692
            if "REPORT RequestId" in message:
5✔
2693
                continue
5✔
2694
            if "END RequestId" in message:
5✔
2695
                continue
5✔
2696

2697
            if not colorize and not force_colorize:
5✔
2698
                if http:
5✔
2699
                    if self.is_http_log_entry(message.strip()):
5✔
2700
                        print("[" + str(timestamp) + "] " + message.strip())
5✔
2701
                elif non_http:
5✔
2702
                    if not self.is_http_log_entry(message.strip()):
×
2703
                        print("[" + str(timestamp) + "] " + message.strip())
×
2704
                else:
2705
                    print("[" + str(timestamp) + "] " + message.strip())
5✔
2706
            else:
2707
                if http:
5✔
2708
                    if self.is_http_log_entry(message.strip()):
5✔
2709
                        click.echo(
5✔
2710
                            click.style("[", fg="cyan")
2711
                            + click.style(str(timestamp), bold=True)
2712
                            + click.style("]", fg="cyan")
2713
                            + self.colorize_log_entry(message.strip()),
2714
                            color=force_colorize,
2715
                        )
2716
                elif non_http:
5✔
2717
                    if not self.is_http_log_entry(message.strip()):
5✔
2718
                        click.echo(
5✔
2719
                            click.style("[", fg="cyan")
2720
                            + click.style(str(timestamp), bold=True)
2721
                            + click.style("]", fg="cyan")
2722
                            + self.colorize_log_entry(message.strip()),
2723
                            color=force_colorize,
2724
                        )
2725
                else:
2726
                    click.echo(
5✔
2727
                        click.style("[", fg="cyan")
2728
                        + click.style(str(timestamp), bold=True)
2729
                        + click.style("]", fg="cyan")
2730
                        + self.colorize_log_entry(message.strip()),
2731
                        color=force_colorize,
2732
                    )
2733

2734
    def is_http_log_entry(self, string):
5✔
2735
        """
2736
        Determines if a log entry is an HTTP-formatted log string or not.
2737
        """
2738
        # Debug event filter
2739
        if "Zappa Event" in string:
5✔
2740
            return False
5✔
2741

2742
        # IP address filter
2743
        for token in string.replace("\t", " ").split(" "):
5✔
2744
            try:
5✔
2745
                if token.count(".") == 3 and token.replace(".", "").isnumeric():
5✔
2746
                    return True
5✔
2747
            except Exception:  # pragma: no cover
2748
                pass
2749

2750
        return False
5✔
2751

2752
    def get_project_name(self):
5✔
2753
        return slugify.slugify(os.getcwd().split(os.sep)[-1])[:15]
5✔
2754

2755
    def colorize_log_entry(self, string):
5✔
2756
        """
2757
        Apply various heuristics to return a colorized version of a string.
2758
        If these fail, simply return the string in plaintext.
2759
        """
2760

2761
        final_string = string
5✔
2762
        try:
5✔
2763
            # First, do stuff in square brackets
2764
            inside_squares = re.findall(r"\[([^]]*)\]", string)
5✔
2765
            for token in inside_squares:
5✔
2766
                if token in ["CRITICAL", "ERROR", "WARNING", "DEBUG", "INFO", "NOTSET"]:
5✔
2767
                    final_string = final_string.replace(
5✔
2768
                        "[" + token + "]",
2769
                        click.style("[", fg="cyan") + click.style(token, fg="cyan", bold=True) + click.style("]", fg="cyan"),
2770
                    )
2771
                else:
2772
                    final_string = final_string.replace(
5✔
2773
                        "[" + token + "]",
2774
                        click.style("[", fg="cyan") + click.style(token, bold=True) + click.style("]", fg="cyan"),
2775
                    )
2776

2777
            # Then do quoted strings
2778
            quotes = re.findall(r'"[^"]*"', string)
5✔
2779
            for token in quotes:
5✔
2780
                final_string = final_string.replace(token, click.style(token, fg="yellow"))
5✔
2781

2782
            # And UUIDs
2783
            for token in final_string.replace("\t", " ").split(" "):
5✔
2784
                try:
5✔
2785
                    if token.count("-") == 4 and token.replace("-", "").isalnum():
5✔
2786
                        final_string = final_string.replace(token, click.style(token, fg="magenta"))
5✔
2787
                except Exception:  # pragma: no cover
2788
                    pass
2789

2790
                # And IP addresses
2791
                try:
5✔
2792
                    if token.count(".") == 3 and token.replace(".", "").isnumeric():
5✔
2793
                        final_string = final_string.replace(token, click.style(token, fg="red"))
5✔
2794
                except Exception:  # pragma: no cover
2795
                    pass
2796

2797
                # And status codes
2798
                try:
5✔
2799
                    if token in ["200"]:
5✔
2800
                        final_string = final_string.replace(token, click.style(token, fg="green"))
5✔
2801
                    if token in ["400", "401", "403", "404", "405", "500"]:
5✔
2802
                        final_string = final_string.replace(token, click.style(token, fg="red"))
5✔
2803
                except Exception:  # pragma: no cover
2804
                    pass
2805

2806
            # And Zappa Events
2807
            try:
5✔
2808
                if "Zappa Event:" in final_string:
5✔
2809
                    final_string = final_string.replace(
5✔
2810
                        "Zappa Event:",
2811
                        click.style("Zappa Event:", bold=True, fg="green"),
2812
                    )
2813
            except Exception:  # pragma: no cover
2814
                pass
2815

2816
            # And dates
2817
            for token in final_string.split("\t"):
5✔
2818
                try:
5✔
2819
                    final_string = final_string.replace(token, click.style(token, fg="green"))
5✔
2820
                except Exception:  # pragma: no cover
2821
                    pass
2822

2823
            final_string = final_string.replace("\t", " ").replace("   ", " ")
5✔
2824
            if final_string[0] != " ":
5✔
2825
                final_string = " " + final_string
5✔
2826
            return final_string
5✔
2827
        except Exception:  # pragma: no cover
2828
            return string
2829

2830
    def execute_prebuild_script(self):
5✔
2831
        """
2832
        Parse and execute the prebuild_script from the zappa_settings.
2833
        """
2834

2835
        (pb_mod_path, pb_func) = self.prebuild_script.rsplit(".", 1)
5✔
2836

2837
        try:  # Prefer prebuild script in working directory
5✔
2838
            if pb_mod_path.count(".") >= 1:  # Prebuild script func is nested in a folder
5✔
2839
                (mod_folder_path, mod_name) = pb_mod_path.rsplit(".", 1)
5✔
2840
                mod_folder_path_fragments = mod_folder_path.split(".")
5✔
2841
                working_dir = os.path.join(os.getcwd(), *mod_folder_path_fragments)
5✔
2842
            else:
2843
                mod_name = pb_mod_path
×
2844
                working_dir = os.getcwd()
×
2845

2846
            working_dir_importer = pkgutil.get_importer(working_dir)
5✔
2847
            module_ = working_dir_importer.find_spec(mod_name).loader.load_module(mod_name)
5✔
2848

2849
        except (ImportError, AttributeError):
×
2850
            try:  # Prebuild func might be in virtualenv
×
2851
                module_ = importlib.import_module(pb_mod_path)
×
2852
            except ImportError:  # pragma: no cover
2853
                raise ClickException(
2854
                    click.style("Failed ", fg="red")
2855
                    + "to "
2856
                    + click.style("import prebuild script ", bold=True)
2857
                    + 'module: "{pb_mod_path}"'.format(pb_mod_path=click.style(pb_mod_path, bold=True))
2858
                )
2859

2860
        if not hasattr(module_, pb_func):  # pragma: no cover
2861
            raise ClickException(
2862
                click.style("Failed ", fg="red")
2863
                + "to "
2864
                + click.style("find prebuild script ", bold=True)
2865
                + 'function: "{pb_func}" '.format(pb_func=click.style(pb_func, bold=True))
2866
                + 'in module "{pb_mod_path}"'.format(pb_mod_path=pb_mod_path)
2867
            )
2868

2869
        prebuild_function = getattr(module_, pb_func)
5✔
2870
        prebuild_function()  # Call the function
5✔
2871

2872
    def collision_warning(self, item):
5✔
2873
        """
2874
        Given a string, print a warning if this could
2875
        collide with a Zappa core package module.
2876
        Use for app functions and events.
2877
        """
2878

2879
        namespace_collisions = [
5✔
2880
            "zappa.",
2881
            "wsgi.",
2882
            "middleware.",
2883
            "handler.",
2884
            "util.",
2885
            "letsencrypt.",
2886
            "cli.",
2887
        ]
2888
        for namespace_collision in namespace_collisions:
5✔
2889
            if item.startswith(namespace_collision):
5✔
2890
                click.echo(
5✔
2891
                    click.style("Warning!", fg="red", bold=True)
2892
                    + " You may have a namespace collision between "
2893
                    + click.style(item, bold=True)
2894
                    + " and "
2895
                    + click.style(namespace_collision, bold=True)
2896
                    + "! You may want to rename that file."
2897
                )
2898

2899
    def deploy_api_gateway(self, api_id):
5✔
2900
        cache_cluster_enabled = self.stage_config.get("cache_cluster_enabled", False)
5✔
2901
        cache_cluster_size = str(self.stage_config.get("cache_cluster_size", 0.5))
5✔
2902
        endpoint_url = self.zappa.deploy_api_gateway(
5✔
2903
            api_id=api_id,
2904
            stage_name=self.api_stage,
2905
            cache_cluster_enabled=cache_cluster_enabled,
2906
            cache_cluster_size=cache_cluster_size,
2907
            cloudwatch_log_level=self.stage_config.get("cloudwatch_log_level", "OFF"),
2908
            cloudwatch_data_trace=self.stage_config.get("cloudwatch_data_trace", False),
2909
            cloudwatch_metrics_enabled=self.stage_config.get("cloudwatch_metrics_enabled", False),
2910
            cache_cluster_ttl=self.stage_config.get("cache_cluster_ttl", 300),
2911
            cache_cluster_encrypted=self.stage_config.get("cache_cluster_encrypted", False),
2912
        )
2913
        return endpoint_url
5✔
2914

2915
    def check_venv(self):
5✔
2916
        """Ensure we're inside a virtualenv."""
2917
        if self.vargs and self.vargs.get("no_venv"):
5✔
2918
            return
×
2919
        if self.zappa:
5✔
2920
            venv = self.zappa.get_current_venv()
5✔
2921
        else:
2922
            # Just for `init`, when we don't have settings yet.
2923
            venv = Zappa.get_current_venv()
×
2924
        if not venv:
5✔
2925
            raise ClickException(
×
2926
                click.style("Zappa", bold=True)
2927
                + " requires an "
2928
                + click.style("active virtual environment", bold=True, fg="red")
2929
                + "!\n"
2930
                + "Learn more about virtual environments here: "
2931
                + click.style(
2932
                    "http://docs.python-guide.org/en/latest/dev/virtualenvs/",
2933
                    bold=False,
2934
                    fg="cyan",
2935
                )
2936
            )
2937

2938
    def silence(self):
5✔
2939
        """
2940
        Route all stdout to null.
2941
        """
2942

2943
        sys.stdout = open(os.devnull, "w")
×
2944
        sys.stderr = open(os.devnull, "w")
×
2945

2946
    def touch_endpoint(self, endpoint_url):
5✔
2947
        """
2948
        Test the deployed endpoint with a GET request.
2949
        """
2950

2951
        # Private APIGW endpoints most likely can't be reached by a deployer
2952
        # unless they're connected to the VPC by VPN. Instead of trying
2953
        # connect to the service, print a warning and let the user know
2954
        # to check it manually.
2955
        # See: https://github.com/Miserlou/Zappa/pull/1719#issuecomment-471341565
2956
        if "PRIVATE" in self.stage_config.get("endpoint_configuration", []):
×
2957
            print(
×
2958
                click.style("Warning!", fg="yellow", bold=True) + " Since you're deploying a private API Gateway endpoint,"
2959
                " Zappa cannot determine if your function is returning "
2960
                " a correct status code. You should check your API's response"
2961
                " manually before considering this deployment complete."
2962
            )
2963
            return
×
2964

2965
        touch_path = self.stage_config.get("touch_path", "/")
×
2966
        req = requests.get(endpoint_url + touch_path)
×
2967

2968
        # Sometimes on really large packages, it can take 60-90 secs to be
2969
        # ready and requests will return 504 status_code until ready.
2970
        # So, if we get a 504 status code, rerun the request up to 4 times or
2971
        # until we don't get a 504 error
2972
        if req.status_code == 504:
×
2973
            i = 0
×
2974
            status_code = 504
×
2975
            while status_code == 504 and i <= 4:
×
2976
                req = requests.get(endpoint_url + touch_path)
×
2977
                status_code = req.status_code
×
2978
                i += 1
×
2979

2980
        if req.status_code >= 500:
×
2981
            raise ClickException(
×
2982
                click.style("Warning!", fg="red", bold=True)
2983
                + " Status check on the deployed lambda failed."
2984
                + " A GET request to '"
2985
                + touch_path
2986
                + "' yielded a "
2987
                + click.style(str(req.status_code), fg="red", bold=True)
2988
                + " response code."
2989
            )
2990

2991

2992
####################################################################
2993
# Main
2994
####################################################################
2995

2996

2997
def shamelessly_promote():
5✔
2998
    """
2999
    Shamelessly promote our little community.
3000
    """
3001

3002
    click.echo(
5✔
3003
        "Need "
3004
        + click.style("help", fg="green", bold=True)
3005
        + "? Found a "
3006
        + click.style("bug", fg="green", bold=True)
3007
        + "? Let us "
3008
        + click.style("know", fg="green", bold=True)
3009
        + "! :D"
3010
    )
3011
    click.echo(
5✔
3012
        "File bug reports on "
3013
        + click.style("GitHub", bold=True)
3014
        + " here: "
3015
        + click.style("https://github.com/Zappa/Zappa", fg="cyan", bold=True)
3016
    )
3017
    click.echo(
5✔
3018
        "And join our "
3019
        + click.style("Slack", bold=True)
3020
        + " channel here: "
3021
        + click.style("https://zappateam.slack.com", fg="cyan", bold=True)
3022
    )
3023
    click.echo("Love!,")
5✔
3024
    click.echo(" ~ Team " + click.style("Zappa", bold=True) + "!")
5✔
3025

3026

3027
def disable_click_colors():
5✔
3028
    """
3029
    Set a Click context where colors are disabled. Creates a throwaway BaseCommand
3030
    to play nicely with the Context constructor.
3031
    The intended side-effect here is that click.echo() checks this context and will
3032
    suppress colors.
3033
    https://github.com/pallets/click/blob/e1aa43a3/click/globals.py#L39
3034
    """
3035

3036
    ctx = Context(BaseCommand("AllYourBaseAreBelongToUs"))
5✔
3037
    ctx.color = False
5✔
3038
    push_context(ctx)
5✔
3039

3040

3041
def handle():  # pragma: no cover
3042
    """
3043
    Main program execution handler.
3044
    """
3045

3046
    try:
3047
        cli = ZappaCLI()
3048
        sys.exit(cli.handle())
3049
    except SystemExit as e:  # pragma: no cover
3050
        cli.on_exit()
3051
        sys.exit(e.code)
3052

3053
    except KeyboardInterrupt:  # pragma: no cover
3054
        cli.on_exit()
3055
        sys.exit(130)
3056
    except Exception:
3057
        cli.on_exit()
3058

3059
        click.echo("Oh no! An " + click.style("error occurred", fg="red", bold=True) + "! :(")
3060
        click.echo("\n==============\n")
3061
        import traceback
3062

3063
        traceback.print_exc()
3064
        click.echo("\n==============\n")
3065
        shamelessly_promote()
3066

3067
        sys.exit(-1)
3068

3069

3070
if __name__ == "__main__":  # pragma: no cover
3071
    handle()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc