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

zappa / Zappa / 15133616794

20 May 2025 09:12AM UTC coverage: 74.696% (-0.2%) from 74.885%
15133616794

push

github

web-flow
fixed conflict(Feature: Handle V2 Event #1182) (#1377)

* :wrench: migrate https://github.com/zappa/Zappa/pull/971 to lastest master

* :art: run black/isort

* :recycle: refactor to allow for other binary ignore types based on mimetype. (currently openapi schema can't be passed as text.

* :art: run black/fix flake8

* :wrench: add EXCEPTION_HANDLER setting

* :bug: fix zappa_returndict["body"] assignment

* :pencil: add temp debug info

* :fire: delete unnecessary print statements

* :recycle: Update comments and minor refactor for clarity

* :recycle: refactor for ease of testing and clarity

* :art: fix flake8

* :sparkles: add `additional_text_mimetypes` setting
:white_check_mark: add testcases for additional_text_mimetypes handling

* :wrench: Expand default text mimetypes mentioned in https://github.com/zappa/Zappa/pull/1023
:recycle: define "DEFAULT_TEXT_MIMETYPES" and move to utilities.py

* :art: run black/isort

* :art: run black/isort

* feat: implement handler for event with format  version 2.0

* refactor: getting processed response body from new method

* fix: lint error

* chore: move variable initialization before initial if condition

* refactor: abstract implementations to two processing methods

* fix: determine payload version based on value in the event itself

* fixed lint

---------

Co-authored-by: monkut <shane.cousins@gmail.com>
Co-authored-by: Rehan Hawari <rehan.hawari10@gmail.com>

72 of 97 new or added lines in 2 files covered. (74.23%)

4 existing lines in 2 files now uncovered.

2822 of 3778 relevant lines covered (74.7%)

3.72 hits per line

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

64.08
/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
    snap_start = None
5✔
123
    context_header_mappings = None
5✔
124
    additional_text_mimetypes = None
5✔
125
    tags = []  # type: ignore[var-annotated]
5✔
126
    layers = None
5✔
127

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

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

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

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

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

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

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

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

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

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

170
        return settings
5✔
171

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

483
        self.command = args.command
×
484

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

639
    ##
640
    # The Commands
641
    ##
642

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

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

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

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

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

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

688
        self.zappa.credentials_arn = role_arn
×
689

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

915
        click.echo(deployment_string)
5✔
916

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1094
            api_id = self.zappa.get_api_id(self.lambda_name)
5✔
1095

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

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

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

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

1117
        else:
1118
            endpoint_url = None
×
1119

1120
        self.schedule()
5✔
1121

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

1126
        self.callback("post")
5✔
1127

1128
        if endpoint_url and "https://" not in endpoint_url:
5✔
1129
            endpoint_url = "https://" + endpoint_url
×
1130

1131
        if self.base_path:
5✔
1132
            endpoint_url += "/" + self.base_path
×
1133

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

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

1142
                if endpoint_url != api_url:
×
1143
                    deployed_string = deployed_string + " (" + api_url + ")"
×
1144

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

1152
        click.echo(deployed_string)
5✔
1153

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

1159
        print("Rolling back..")
5✔
1160

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

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

1180
        try:
5✔
1181
            since_stamp = string_to_timestamp(since)
5✔
1182

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

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

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

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

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

1217
        if self.use_alb:
5✔
1218
            self.zappa.undeploy_lambda_alb(self.lambda_name)
×
1219

1220
        if self.use_apigateway:
5✔
1221
            if remove_logs:
5✔
1222
                self.zappa.remove_api_gateway_logs(self.lambda_name)
5✔
1223

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

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

1232
            self.zappa.undeploy_api_gateway(self.lambda_name, domain_name=domain_name, base_path=base_path)
5✔
1233

1234
        self.unschedule()  # removes event triggers, including warm up event.
5✔
1235

1236
        self.zappa.delete_lambda_function(self.lambda_name)
5✔
1237
        if remove_logs:
5✔
1238
            self.zappa.remove_lambda_function_logs(self.lambda_name)
5✔
1239

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1381
        # Can't use hjson
1382
        import json as json
×
1383

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

1390
        print(self.format_lambda_response(response, not no_color))
×
1391

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

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

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

1419
        string = string.replace("\\n", "\n")
5✔
1420

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

1430
        return formated_response
5✔
1431

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

1439
        final_string = string
5✔
1440

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

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

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

1481
            return final_string
5✔
1482
        except Exception:
×
1483
            return string
×
1484

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

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

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

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

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

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

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

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

1568
            status_dict["API Gateway URL"] = api_url
5✔
1569

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

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

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

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

1615
        # TODO: S3/SQS/etc. type events?
1616

1617
        return True
5✔
1618

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

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

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

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

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

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

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

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

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

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

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

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

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

1755
        profile_region = profile.get("region") if profile else None
×
1756

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

1766
            if is_valid_bucket_name(bucket):
×
1767
                break
×
1768

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

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

1792
            has_django = True
1793
        except ImportError:
×
1794
            has_django = False
×
1795

1796
        try:  # pragma: no cover
1797
            import flask  # noqa: F401
1798

1799
            has_flask = True
1800
        except ImportError:
×
1801
            has_flask = False
×
1802

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

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

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

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

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

1888
        if profile_region:
×
1889
            zappa_settings[env]["aws_region"] = profile_region
×
1890

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

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

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

1908
        import json as json  # hjson is fine for loading, not fine for writing.
×
1909

1910
        zappa_settings_json = json.dumps(zappa_settings, sort_keys=True, indent=4)
×
1911

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

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

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

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

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

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

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

1964
        return
×
1965

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

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

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

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

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

1999
        # These are sensitive
2000
        certificate_body = None
5✔
2001
        certificate_private_key = None
5✔
2002
        certificate_chain = None
5✔
2003

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

2017
            # Get install account_key to /tmp/account_key.pem
2018
            from .letsencrypt import gettempdir
×
2019

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

2026
                copyfile(account_key_location, os.path.join(gettempdir(), "account.key"))
×
2027

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

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

2045
            with open(cert_key_location) as f:
5✔
2046
                certificate_private_key = f.read()
5✔
2047

2048
            with open(cert_chain_location) as f:
5✔
2049
                certificate_chain = f.read()
5✔
2050

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

2053
        # Get cert and update domain.
2054

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

2059
            cert_success = get_cert_and_update_domain(self.zappa, self.lambda_name, self.api_stage, self.domain, manual)
×
2060

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

2097
            cert_success = True
5✔
2098

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

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

2124
    ##
2125
    # Utility
2126
    ##
2127

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

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

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

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

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

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

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

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

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

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

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

2214
        # Load up file
2215
        self.load_settings_file(settings_file)
5✔
2216

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

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

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

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

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

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

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

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

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

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

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

2319
        # Additional tags
2320
        self.tags = self.stage_config.get("tags", {})
5✔
2321

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

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

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

2355
        return self.zappa
5✔
2356

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

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

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

2385
        return settings_file
5✔
2386

2387
    def load_settings_file(self, settings_file=None):
5✔
2388
        """
2389
        Load our settings file.
2390
        """
2391

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

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

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

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

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

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

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

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

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

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

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

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

2502
    def get_zappa_settings_string(self):
5✔
2503
        settings_s = "# Generated by Zappa\n"
5✔
2504

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

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

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

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

2530
        if self.binary_support:
5✔
2531
            settings_s = settings_s + "BINARY_SUPPORT=True\n"
5✔
2532
        else:
2533
            settings_s = settings_s + "BINARY_SUPPORT=False\n"
×
2534

2535
        head_map_dict = {}
5✔
2536
        head_map_dict.update(dict(self.context_header_mappings))
5✔
2537
        settings_s = settings_s + "CONTEXT_HEADER_MAPPINGS={0}\n".format(head_map_dict)
5✔
2538

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2651
    def remove_local_zip(self):
5✔
2652
        """
2653
        Remove our local zip file.
2654
        """
2655

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

2665
    def remove_uploaded_zip(self):
5✔
2666
        """
2667
        Remove the local and S3 zip file after uploading and updating.
2668
        """
2669

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

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

2687
            self.remove_local_zip()
5✔
2688

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

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

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

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

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

2757
        return False
5✔
2758

2759
    def get_project_name(self):
5✔
2760
        return slugify.slugify(os.getcwd().split(os.sep)[-1])[:15]
5✔
2761

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

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

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

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

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

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

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

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

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

2837
    def execute_prebuild_script(self):
5✔
2838
        """
2839
        Parse and execute the prebuild_script from the zappa_settings.
2840
        """
2841

2842
        (pb_mod_path, pb_func) = self.prebuild_script.rsplit(".", 1)
5✔
2843

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

2853
            working_dir_importer = pkgutil.get_importer(working_dir)
5✔
2854
            module_ = working_dir_importer.find_spec(mod_name).loader.load_module(mod_name)
5✔
2855

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

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

2876
        prebuild_function = getattr(module_, pb_func)
5✔
2877
        prebuild_function()  # Call the function
5✔
2878

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

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

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

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

2945
    def silence(self):
5✔
2946
        """
2947
        Route all stdout to null.
2948
        """
2949

2950
        sys.stdout = open(os.devnull, "w")
×
2951
        sys.stderr = open(os.devnull, "w")
×
2952

2953
    def touch_endpoint(self, endpoint_url):
5✔
2954
        """
2955
        Test the deployed endpoint with a GET request.
2956
        """
2957

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

2972
        touch_path = self.stage_config.get("touch_path", "/")
×
2973
        req = requests.get(endpoint_url + touch_path)
×
2974

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

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

2998

2999
####################################################################
3000
# Main
3001
####################################################################
3002

3003

3004
def shamelessly_promote():
5✔
3005
    """
3006
    Shamelessly promote our little community.
3007
    """
3008

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

3033

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

3043
    ctx = Context(BaseCommand("AllYourBaseAreBelongToUs"))
5✔
3044
    ctx.color = False
5✔
3045
    push_context(ctx)
5✔
3046

3047

3048
def handle():  # pragma: no cover
3049
    """
3050
    Main program execution handler.
3051
    """
3052

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

3060
    except KeyboardInterrupt:  # pragma: no cover
3061
        cli.on_exit()
3062
        sys.exit(130)
3063
    except Exception:
3064
        cli.on_exit()
3065

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

3070
        traceback.print_exc()
3071
        click.echo("\n==============\n")
3072
        shamelessly_promote()
3073

3074
        sys.exit(-1)
3075

3076

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

© 2025 Coveralls, Inc