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

zappa / Zappa / 8825766175

25 Apr 2024 01:45AM UTC coverage: 74.668% (-0.1%) from 74.81%
8825766175

push

github

web-flow
Update cli.py (#1314)

Expected Behavior
The CLI should output that the current AWS user does not have permission to call lambda:GetFunction.

Actual Behavior
The CLI outputs: Warning! Couldn't get function {{function_name}} in {{AWS Region}} - have you deployed yet?.

Co-authored-by: monkut <shane.cousins@gmail.com>

0 of 3 new or added lines in 1 file covered. (0.0%)

3 existing lines in 1 file now uncovered.

2759 of 3695 relevant lines covered (74.67%)

3.72 hits per line

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

64.03
/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
            # https://github.com/zappa/Zappa/issues/1313
NEW
940
            except botocore.exceptions.ClientError as e:
×
NEW
941
                click.echo(click.style(type(e).__name__, fg="red") + ": " + e.args[0])
×
NEW
942
                sys.exit(-1)
×
943
            except Exception:
×
944
                click.echo(
×
945
                    click.style("Warning!", fg="red")
946
                    + " Couldn't get function "
947
                    + self.lambda_name
948
                    + " in "
949
                    + self.zappa.aws_region
950
                    + " - have you deployed yet?"
951
                )
952
                sys.exit(-1)
×
953

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

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

982
            # Create the Lambda Zip,
983
            if not no_upload:
5✔
984
                self.create_package()
5✔
985
                self.callback("zip")
5✔
986

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

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

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

1018
                    handler_file = self.handler_path
×
1019
                else:
1020
                    handler_file = self.zip_path
5✔
1021

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

1047
        # Remove the uploaded zip from S3, because it is now registered..
1048
        if not source_zip and not no_upload and not docker_image_uri:
5✔
1049
            self.remove_uploaded_zip()
5✔
1050

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

1068
        # Finally, delete the local copy our zip package
1069
        if not source_zip and not no_upload and not docker_image_uri:
5✔
1070
            if self.stage_config.get("delete_local_zip", True):
5✔
1071
                self.remove_local_zip()
5✔
1072

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

1092
            api_id = self.zappa.get_api_id(self.lambda_name)
5✔
1093

1094
            # Update binary support
1095
            if self.binary_support:
5✔
1096
                self.zappa.add_binary_support(api_id=api_id, cors=self.cors)
5✔
1097
            else:
1098
                self.zappa.remove_binary_support(api_id=api_id, cors=self.cors)
×
1099

1100
            if self.stage_config.get("payload_compression", True):
5✔
1101
                self.zappa.add_api_compression(
5✔
1102
                    api_id=api_id,
1103
                    min_compression_size=self.stage_config.get("payload_minimum_compression_size", 0),
1104
                )
1105
            else:
1106
                self.zappa.remove_api_compression(api_id=api_id)
×
1107

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

1112
            if self.stage_config.get("domain", None):
5✔
1113
                endpoint_url = self.stage_config.get("domain")
×
1114

1115
        else:
1116
            endpoint_url = None
×
1117

1118
        self.schedule()
5✔
1119

1120
        # Update any cognito pool with the lambda arn
1121
        # do this after schedule as schedule clears the lambda policy and we need to add one
1122
        self.update_cognito_triggers()
5✔
1123

1124
        self.callback("post")
5✔
1125

1126
        if endpoint_url and "https://" not in endpoint_url:
5✔
1127
            endpoint_url = "https://" + endpoint_url
×
1128

1129
        if self.base_path:
5✔
1130
            endpoint_url += "/" + self.base_path
×
1131

1132
        deployed_string = "Your updated Zappa deployment is " + click.style("live", fg="green", bold=True) + "!"
5✔
1133
        if self.use_apigateway:
5✔
1134
            deployed_string = deployed_string + ": " + click.style("{}".format(endpoint_url), bold=True)
5✔
1135

1136
            api_url = None
5✔
1137
            if endpoint_url and "amazonaws.com" not in endpoint_url:
5✔
1138
                api_url = self.zappa.get_api_url(self.lambda_name, self.api_stage)
×
1139

1140
                if endpoint_url != api_url:
×
1141
                    deployed_string = deployed_string + " (" + api_url + ")"
×
1142

1143
            if self.stage_config.get("touch", True):
5✔
1144
                self.zappa.wait_until_lambda_function_is_updated(function_name=self.lambda_name)
×
1145
                if api_url:
×
1146
                    self.touch_endpoint(api_url)
×
1147
                elif endpoint_url:
×
1148
                    self.touch_endpoint(endpoint_url)
×
1149

1150
        click.echo(deployed_string)
5✔
1151

1152
    def rollback(self, revision):
5✔
1153
        """
1154
        Rollsback the currently deploy lambda code to a previous revision.
1155
        """
1156

1157
        print("Rolling back..")
5✔
1158

1159
        self.zappa.rollback_lambda_function_version(self.lambda_name, versions_back=revision)
5✔
1160
        print("Done!")
5✔
1161

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

1178
        try:
5✔
1179
            since_stamp = string_to_timestamp(since)
5✔
1180

1181
            last_since = since_stamp
5✔
1182
            while True:
3✔
1183
                new_logs = self.zappa.fetch_logs(
5✔
1184
                    self.lambda_name,
1185
                    start_time=since_stamp,
1186
                    limit=limit,
1187
                    filter_pattern=filter_pattern,
1188
                )
1189

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

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

1205
    def undeploy(self, no_confirm=False, remove_logs=False):
5✔
1206
        """
1207
        Tear down an existing deployment.
1208
        """
1209

1210
        if not no_confirm:  # pragma: no cover
1211
            confirm = input("Are you sure you want to undeploy? [y/n] ")
1212
            if confirm != "y":
1213
                return
1214

1215
        if self.use_alb:
5✔
1216
            self.zappa.undeploy_lambda_alb(self.lambda_name)
×
1217

1218
        if self.use_apigateway:
5✔
1219
            if remove_logs:
5✔
1220
                self.zappa.remove_api_gateway_logs(self.lambda_name)
5✔
1221

1222
            domain_name = self.stage_config.get("domain", None)
5✔
1223
            base_path = self.stage_config.get("base_path", None)
5✔
1224

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

1230
            self.zappa.undeploy_api_gateway(self.lambda_name, domain_name=domain_name, base_path=base_path)
5✔
1231

1232
        self.unschedule()  # removes event triggers, including warm up event.
5✔
1233

1234
        self.zappa.delete_lambda_function(self.lambda_name)
5✔
1235
        if remove_logs:
5✔
1236
            self.zappa.remove_lambda_function_logs(self.lambda_name)
5✔
1237

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

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

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

1259
        if events:
5✔
1260
            if not isinstance(events, list):  # pragma: no cover
1261
                print("Events must be supplied as a list.")
1262
                return
1263

1264
        for event in events:
5✔
1265
            self.collision_warning(event.get("function"))
5✔
1266

1267
        if self.stage_config.get("keep_warm", True):
5✔
1268
            if not events:
5✔
1269
                events = []
×
1270

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

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

1294
            print("Scheduling..")
5✔
1295
            self.zappa.schedule_events(
5✔
1296
                lambda_arn=function_response["Configuration"]["FunctionArn"],
1297
                lambda_name=self.lambda_name,
1298
                events=events,
1299
            )
1300

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

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

1329
    def unschedule(self):
5✔
1330
        """
1331
        Given a a list of scheduled functions,
1332
        tear down their regular execution.
1333
        """
1334

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

1338
        if not isinstance(events, list):  # pragma: no cover
1339
            print("Events must be supplied as a list.")
1340
            return
1341

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

1352
        print("Unscheduling..")
5✔
1353
        self.zappa.unschedule_events(
5✔
1354
            lambda_name=self.lambda_name,
1355
            lambda_arn=function_arn,
1356
            events=events,
1357
        )
1358

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

1364
    def invoke(self, function_name, raw_python=False, command=None, no_color=False):
5✔
1365
        """
1366
        Invoke a remote function.
1367
        """
1368

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

1379
        # Can't use hjson
1380
        import json as json
×
1381

1382
        response = self.zappa.invoke_lambda_function(
×
1383
            self.lambda_name,
1384
            json.dumps(command),
1385
            invocation_type="RequestResponse",
1386
        )
1387

1388
        print(self.format_lambda_response(response, not no_color))
×
1389

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

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

1411
    def format_invoke_command(self, string):
5✔
1412
        """
1413
        Formats correctly the string output from the invoke() method,
1414
        replacing line breaks and tabs when necessary.
1415
        """
1416

1417
        string = string.replace("\\n", "\n")
5✔
1418

1419
        formated_response = ""
5✔
1420
        for line in string.splitlines():
5✔
1421
            if line.startswith("REPORT"):
5✔
1422
                line = line.replace("\t", "\n")
5✔
1423
            if line.startswith("[DEBUG]"):
5✔
1424
                line = line.replace("\t", " ")
5✔
1425
            formated_response += line + "\n"
5✔
1426
        formated_response = formated_response.replace("\n\n", "\n")
5✔
1427

1428
        return formated_response
5✔
1429

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

1437
        final_string = string
5✔
1438

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

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

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

1479
            return final_string
5✔
1480
        except Exception:
×
1481
            return string
×
1482

1483
    def status(self, return_json=False):
5✔
1484
        """
1485
        Describe the status of the current deployment.
1486
        """
1487

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

1495
        # Lambda Env Details
1496
        lambda_versions = self.zappa.get_lambda_function_versions(self.lambda_name)
5✔
1497

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

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

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

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

1562
        # URLs
1563
        if self.use_apigateway:
5✔
1564
            api_url = self.zappa.get_api_url(self.lambda_name, self.api_stage)
5✔
1565

1566
            status_dict["API Gateway URL"] = api_url
5✔
1567

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

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

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

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

1613
        # TODO: S3/SQS/etc. type events?
1614

1615
        return True
5✔
1616

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

1631
    def check_environment(self, environment):
5✔
1632
        """
1633
        Make sure the environment contains only strings
1634
        (since putenv needs a string)
1635
        """
1636

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

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

1653
        # Make sure we're in a venv.
1654
        self.check_venv()
×
1655

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

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

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

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

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

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

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

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

1753
        profile_region = profile.get("region") if profile else None
×
1754

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

1764
            if is_valid_bucket_name(bucket):
×
1765
                break
×
1766

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

1786
        # Detect Django/Flask
1787
        try:  # pragma: no cover
1788
            import django  # noqa: F401
1789

1790
            has_django = True
1791
        except ImportError:
×
1792
            has_django = False
×
1793

1794
        try:  # pragma: no cover
1795
            import flask  # noqa: F401
1796

1797
            has_flask = True
1798
        except ImportError:
×
1799
            has_flask = False
×
1800

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

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

1847
        # TODO: Create VPC?
1848
        # Memory size? Time limit?
1849
        # Domain? LE keys? Region?
1850
        # 'Advanced Settings' mode?
1851

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

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

1886
        if profile_region:
×
1887
            zappa_settings[env]["aws_region"] = profile_region
×
1888

1889
        if has_django:
×
1890
            zappa_settings[env]["django_settings"] = django_settings
×
1891
        else:
1892
            zappa_settings[env]["app_function"] = app_function
×
1893

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

1901
            for region in additional_regions:
×
1902
                env_name = env + "_" + region.replace("-", "_")
×
1903
                g_env = {env_name: {"extends": env, "aws_region": region}}
×
1904
                zappa_settings.update(g_env)
×
1905

1906
        import json as json  # hjson is fine for loading, not fine for writing.
×
1907

1908
        zappa_settings_json = json.dumps(zappa_settings, sort_keys=True, indent=4)
×
1909

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

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

1918
        # Write
1919
        with open("zappa_settings.json", "w") as zappa_settings_file:
×
1920
            zappa_settings_file.write(zappa_settings_json)
×
1921

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

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

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

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

1962
        return
×
1963

1964
    def certify(self, no_confirm=True, manual=False):
5✔
1965
        """
1966
        Register or update a domain certificate for this env.
1967
        """
1968

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

1974
        if not no_confirm:  # pragma: no cover
1975
            confirm = input("Are you sure you want to certify? [y/n] ")
1976
            if confirm != "y":
1977
                return
1978

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

1990
        account_key_location = self.stage_config.get("lets_encrypt_key", None)
5✔
1991
        cert_location = self.stage_config.get("certificate", None)
5✔
1992
        cert_key_location = self.stage_config.get("certificate_key", None)
5✔
1993
        cert_chain_location = self.stage_config.get("certificate_chain", None)
5✔
1994
        cert_arn = self.stage_config.get("certificate_arn", None)
5✔
1995
        base_path = self.stage_config.get("base_path", None)
5✔
1996

1997
        # These are sensitive
1998
        certificate_body = None
5✔
1999
        certificate_private_key = None
5✔
2000
        certificate_chain = None
5✔
2001

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

2015
            # Get install account_key to /tmp/account_key.pem
2016
            from .letsencrypt import gettempdir
×
2017

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

2024
                copyfile(account_key_location, os.path.join(gettempdir(), "account.key"))
×
2025

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

2039
            # Read the supplied certificates.
2040
            with open(cert_location) as f:
5✔
2041
                certificate_body = f.read()
5✔
2042

2043
            with open(cert_key_location) as f:
5✔
2044
                certificate_private_key = f.read()
5✔
2045

2046
            with open(cert_chain_location) as f:
5✔
2047
                certificate_chain = f.read()
5✔
2048

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

2051
        # Get cert and update domain.
2052

2053
        # Let's Encrypt
2054
        if not cert_location and not cert_arn:
5✔
2055
            from .letsencrypt import get_cert_and_update_domain
×
2056

2057
            cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual)
×
2058

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

2095
            cert_success = True
5✔
2096

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

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

2122
    ##
2123
    # Utility
2124
    ##
2125

2126
    def callback(self, position):
5✔
2127
        """
2128
        Allows the execution of custom code between creation of the zip file and deployment to AWS.
2129
        :return: None
2130
        """
2131

2132
        callbacks = self.stage_config.get("callbacks", {})
5✔
2133
        callback = callbacks.get(position)
5✔
2134

2135
        if callback:
5✔
2136
            (mod_path, cb_func_name) = callback.rsplit(".", 1)
5✔
2137

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

2147
                working_dir_importer = pkgutil.get_importer(working_dir)
5✔
2148
                module_ = working_dir_importer.find_spec(mod_name).loader.load_module(mod_name)
5✔
2149

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

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

2173
            cb_func = getattr(module_, cb_func_name)
5✔
2174
            cb_func(self)  # Call the function passing self
5✔
2175

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

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

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

2212
        # Load up file
2213
        self.load_settings_file(settings_file)
5✔
2214

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

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

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

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

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

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

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

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

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

2308
        # check that BINARY_SUPPORT is True if additional_text_mimetypes is provided
2309
        if self.additional_text_mimetypes and not self.binary_support:
5✔
2310
            raise ClickException("zappa_settings.json has additional_text_mimetypes defined, but binary_support is False!")
5✔
2311

2312
        # Load ALB-related settings
2313
        self.use_alb = self.stage_config.get("alb_enabled", False)
5✔
2314
        self.alb_vpc_config = self.stage_config.get("alb_vpc_config", {})
5✔
2315

2316
        # Additional tags
2317
        self.tags = self.stage_config.get("tags", {})
5✔
2318

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

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

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

2352
        return self.zappa
5✔
2353

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

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

2372
        # Prefer JSON
2373
        if os.path.isfile(zs_json):
5✔
2374
            settings_file = zs_json
5✔
2375
        elif os.path.isfile(zs_toml):
5✔
2376
            settings_file = zs_toml
5✔
2377
        elif os.path.isfile(zs_yml):
5✔
2378
            settings_file = zs_yml
5✔
2379
        else:
2380
            settings_file = zs_yaml
5✔
2381

2382
        return settings_file
5✔
2383

2384
    def load_settings_file(self, settings_file=None):
5✔
2385
        """
2386
        Load our settings file.
2387
        """
2388

2389
        if not settings_file:
5✔
2390
            settings_file = self.get_json_or_yaml_settings()
5✔
2391
        if not os.path.isfile(settings_file):
5✔
2392
            raise ClickException("Please configure your zappa_settings file or call `zappa init`.")
×
2393

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

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

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

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

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

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

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

2475
        # Throw custom settings into the zip that handles requests
2476
        if self.stage_config.get("slim_handler", False):
5✔
2477
            handler_zip = self.handler_path
5✔
2478
        else:
2479
            handler_zip = self.zip_path
5✔
2480

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

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

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

2499
    def get_zappa_settings_string(self):
5✔
2500
        settings_s = "# Generated by Zappa\n"
5✔
2501

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

2515
        if self.exception_handler:
5✔
2516
            settings_s += "EXCEPTION_HANDLER='{0!s}'\n".format(self.exception_handler)
×
2517
        else:
2518
            settings_s += "EXCEPTION_HANDLER=None\n"
5✔
2519

2520
        if self.debug:
5✔
2521
            settings_s = settings_s + "DEBUG=True\n"
5✔
2522
        else:
2523
            settings_s = settings_s + "DEBUG=False\n"
×
2524

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

2527
        if self.binary_support:
5✔
2528
            settings_s = settings_s + "BINARY_SUPPORT=True\n"
5✔
2529
        else:
2530
            settings_s = settings_s + "BINARY_SUPPORT=False\n"
×
2531

2532
        head_map_dict = {}
5✔
2533
        head_map_dict.update(dict(self.context_header_mappings))
5✔
2534
        settings_s = settings_s + "CONTEXT_HEADER_MAPPINGS={0}\n".format(head_map_dict)
5✔
2535

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

2543
        if self.base_path:
5✔
2544
            settings_s = settings_s + "BASE_PATH='{0!s}'\n".format((self.base_path))
×
2545
        else:
2546
            settings_s = settings_s + "BASE_PATH=None\n"
5✔
2547

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

2555
        # Local envs
2556
        env_dict = {}
5✔
2557
        if self.aws_region:
5✔
2558
            env_dict["AWS_REGION"] = self.aws_region
×
2559
        env_dict.update(dict(self.environment_variables))
5✔
2560

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

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

2571
        # We can be environment-aware
2572
        settings_s = settings_s + "API_STAGE='{0!s}'\n".format((self.api_stage))
5✔
2573
        settings_s = settings_s + "PROJECT_NAME='{0!s}'\n".format((self.project_name))
5✔
2574

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

2580
        if self.django_settings:
5✔
2581
            settings_s = settings_s + "DJANGO_SETTINGS='{0!s}'\n".format((self.django_settings))
×
2582
        else:
2583
            settings_s = settings_s + "DJANGO_SETTINGS=None\n"
5✔
2584

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

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

2596
            include = self.stage_config.get("include", [])
5✔
2597
            if len(include) >= 1:
5✔
2598
                settings_s += "INCLUDE=" + str(include) + "\n"
×
2599

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

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

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

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

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

2639
        # async response
2640
        async_response_table = self.stage_config.get("async_response_table", "")
5✔
2641
        settings_s += "ASYNC_RESPONSE_TABLE='{0!s}'\n".format(async_response_table)
5✔
2642

2643
        # additional_text_mimetypes
2644
        additional_text_mimetypes = self.stage_config.get("additional_text_mimetypes", [])
5✔
2645
        settings_s += f"ADDITIONAL_TEXT_MIMETYPES={additional_text_mimetypes}\n"
5✔
2646
        return settings_s
5✔
2647

2648
    def remove_local_zip(self):
5✔
2649
        """
2650
        Remove our local zip file.
2651
        """
2652

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

2662
    def remove_uploaded_zip(self):
5✔
2663
        """
2664
        Remove the local and S3 zip file after uploading and updating.
2665
        """
2666

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

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

2684
            self.remove_local_zip()
5✔
2685

2686
    def print_logs(self, logs, colorize=True, http=False, non_http=False, force_colorize=None):
5✔
2687
        """
2688
        Parse, filter and print logs to the console.
2689
        """
2690

2691
        for log in logs:
5✔
2692
            timestamp = log["timestamp"]
5✔
2693
            message = log["message"]
5✔
2694
            if "START RequestId" in message:
5✔
2695
                continue
5✔
2696
            if "REPORT RequestId" in message:
5✔
2697
                continue
5✔
2698
            if "END RequestId" in message:
5✔
2699
                continue
5✔
2700

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

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

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

2754
        return False
5✔
2755

2756
    def get_project_name(self):
5✔
2757
        return slugify.slugify(os.getcwd().split(os.sep)[-1])[:15]
5✔
2758

2759
    def colorize_log_entry(self, string):
5✔
2760
        """
2761
        Apply various heuristics to return a colorized version of a string.
2762
        If these fail, simply return the string in plaintext.
2763
        """
2764

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

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

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

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

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

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

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

2827
            final_string = final_string.replace("\t", " ").replace("   ", " ")
5✔
2828
            if final_string[0] != " ":
5✔
2829
                final_string = " " + final_string
5✔
2830
            return final_string
5✔
2831
        except Exception:  # pragma: no cover
2832
            return string
2833

2834
    def execute_prebuild_script(self):
5✔
2835
        """
2836
        Parse and execute the prebuild_script from the zappa_settings.
2837
        """
2838

2839
        (pb_mod_path, pb_func) = self.prebuild_script.rsplit(".", 1)
5✔
2840

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

2850
            working_dir_importer = pkgutil.get_importer(working_dir)
5✔
2851
            module_ = working_dir_importer.find_spec(mod_name).loader.load_module(mod_name)
5✔
2852

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

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

2873
        prebuild_function = getattr(module_, pb_func)
5✔
2874
        prebuild_function()  # Call the function
5✔
2875

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

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

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

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

2942
    def silence(self):
5✔
2943
        """
2944
        Route all stdout to null.
2945
        """
2946

2947
        sys.stdout = open(os.devnull, "w")
×
2948
        sys.stderr = open(os.devnull, "w")
×
2949

2950
    def touch_endpoint(self, endpoint_url):
5✔
2951
        """
2952
        Test the deployed endpoint with a GET request.
2953
        """
2954

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

2969
        touch_path = self.stage_config.get("touch_path", "/")
×
2970
        req = requests.get(endpoint_url + touch_path)
×
2971

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

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

2995

2996
####################################################################
2997
# Main
2998
####################################################################
2999

3000

3001
def shamelessly_promote():
5✔
3002
    """
3003
    Shamelessly promote our little community.
3004
    """
3005

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

3030

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

3040
    ctx = Context(BaseCommand("AllYourBaseAreBelongToUs"))
5✔
3041
    ctx.color = False
5✔
3042
    push_context(ctx)
5✔
3043

3044

3045
def handle():  # pragma: no cover
3046
    """
3047
    Main program execution handler.
3048
    """
3049

3050
    try:
3051
        cli = ZappaCLI()
3052
        sys.exit(cli.handle())
3053
    except SystemExit as e:  # pragma: no cover
3054
        cli.on_exit()
3055
        sys.exit(e.code)
3056

3057
    except KeyboardInterrupt:  # pragma: no cover
3058
        cli.on_exit()
3059
        sys.exit(130)
3060
    except Exception:
3061
        cli.on_exit()
3062

3063
        click.echo("Oh no! An " + click.style("error occurred", fg="red", bold=True) + "! :(")
3064
        click.echo("\n==============\n")
3065
        import traceback
3066

3067
        traceback.print_exc()
3068
        click.echo("\n==============\n")
3069
        shamelessly_promote()
3070

3071
        sys.exit(-1)
3072

3073

3074
if __name__ == "__main__":  # pragma: no cover
3075
    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