PK!2ךsnakeless/__init__.pyimport sys from .cli_controller import SnakelessCli def main(argv=sys.argv[1:]): snakeless_cli = SnakelessCli() return snakeless_cli.run(argv) PK! <]]snakeless/__main__.pyimport sys from . import main if __name__ == '__main__': sys.exit(main(sys.argv[1:])) PK!8x{``snakeless/actions/__init__.pyfrom .deployer import DeployerMixin # noqa from .config_loader import ConfigLoaderMixin # noqaPK!Cx"snakeless/actions/config_loader.pyimport fs from schema import SchemaError from ..helpers import snakeless_spinner, parse_config from ..exceptions import CommandFailure class ConfigLoaderMixin(object): def load_config(self, root_fs): with snakeless_spinner( text="Loading the config file...", spinner="dots" ) as spinner: try: config = parse_config(root_fs) except fs.errors.ResourceNotFound: raise CommandFailure("Config does not exist.") except SchemaError as exc: raise CommandFailure("Config validation failed.") else: spinner.succeed("The config file was loaded.") return config PK!5snakeless/actions/deployer.pyfrom ..helpers import snakeless_spinner, get_provider class DeployerMixin(object): def deploy_functions(self, config, functions_to_deploy): with snakeless_spinner( text="Deploying functions...", spinner="dots" ) as spinner: provider_name = config["project"]["provider"] provider = get_provider(provider_name, config) for function_to_deploy in functions_to_deploy: spinner.text = ( f"Deploying the { function_to_deploy } function..." ) provider.deploy_function(function_to_deploy) spinner.succeed("All functions were deployed!") PK!v{WKKsnakeless/cli_controller.pyfrom cliff import help as cliff_help from cliff.app import App from cliff.commandmanager import CommandManager class SnakelessCli(App): def __init__(self): super().__init__( description="Snakeless CLI", version="0.1.2", command_manager=CommandManager("snakeless.cli"), deferred_help=True, ) def initialize_app(self, argv): if self.interactive_mode: # disable interactive mode action = cliff_help.HelpAction(None, None, default=self) action(self.parser, self.options, None, None) PK!˶CCsnakeless/commands/__init__.pyfrom .check import Check # noqa from .deploy import Deploy # noqaPK!{00snakeless/commands/check.pyimport logging import fs from cliff.command import Command from schema import SchemaError from ..constants import CURRENT_DIR from ..helpers import ( check_config_existence, snakeless_spinner, parse_config ) from ..exceptions import CommandFailure class Check(Command): "A health-check of your snakeless setup" logger = logging.getLogger(__name__) def check_config_existence(self, root_fs): with snakeless_spinner( text="Checking the config existence.", spinner="dots" ) as spinner: config_file_exists = check_config_existence(root_fs) if not config_file_exists: raise CommandFailure("Config was not found.") else: spinner.succeed("Config was found.") def validate_config(self, root_fs): with snakeless_spinner( text="Validating the config file.", spinner="dots" ) as spinner: try: parse_config(root_fs) except fs.errors.ResourceNotFound: raise CommandFailure("Config does not anymore exist.") except SchemaError as exc: raise CommandFailure("Config validation failed") else: spinner.succeed("Config is valid.") def take_action(self, parsed_args): with fs.open_fs(CURRENT_DIR) as root_fs: try: self.check_config_existence(root_fs) self.validate_config(root_fs) except Exception as exc: self.logger.exception(exc, exc_info=True) PK!  snakeless/commands/deploy.pyimport logging import fs from cliff.command import Command from ..constants import CURRENT_DIR from ..actions import DeployerMixin, ConfigLoaderMixin class Deploy(Command, DeployerMixin, ConfigLoaderMixin): "Deploy some functions or all of themn" logger = logging.getLogger(__name__) def get_parser(self, prog_name): parser = super().get_parser(prog_name) parser.add_argument( "-f", nargs="*", help="A list of functions' names" ) return parser def take_action(self, parsed_args): parsed_args = vars(parsed_args) functions_to_deploy = parsed_args.get('f', []) with fs.open_fs(CURRENT_DIR) as root_fs: try: config = self.load_config(root_fs) if not functions_to_deploy: functions_to_deploy = config['functions'].keys() self.deploy_functions(config, functions_to_deploy) except Exception as exc: self.logger.exception(exc, exc_info=True) PK!G G snakeless/constants.pyimport os from schema import Schema, And, Use, Optional from .providers import GCloudProvider CURRENT_DIR = os.getcwd() SUPPORTED_RUNTIMES = ["python37"] SUPPORTED_PROVIDERS = ["gcloud"] SUPPORTED_MEMORY_SIZES = [128, 256, 512, 1024, 2048] PROVIDERS = { 'gcloud': GCloudProvider } CONFIG_SCHEMA = { "project": { "name": str, "creds": str, "provider": And( Use(str), lambda provider: provider in SUPPORTED_PROVIDERS, error="Unsupported provider", ), "runtime": And( Use(str), lambda runtime: runtime in SUPPORTED_RUNTIMES, error="Unsupported runtime", ), "region": str, Optional("default_stage", default="dev"): str, Optional("api_prefix"): str, Optional("memory_size", default=128): And( Use(int), lambda memory_size: memory_size in SUPPORTED_MEMORY_SIZES, error="Invalid memory size", ), Optional("timeout", default=60): And( Use(int), lambda timeout: timeout > 0 ), Optional("deployment_bucket"): str, Optional("env_file_path", default=".env"): str, }, Optional("package"): { Optional("include"): [str], Optional("exclude"): [str], Optional("exclude_dev_dependencies", default=True): bool, Optional("individually", default=False): bool, }, "functions": { str: { "handler": str, "handler_path": str, "events": [{Optional("http"): {"path": str}}], Optional("name"): str, Optional("description"): str, Optional("memory_size", default=128): And( Use(int), lambda memory_size: memory_size in SUPPORTED_MEMORY_SIZES, error="Invalid memory size", ), Optional("runtime"): And( Use(str), lambda runtime: runtime in SUPPORTED_RUNTIMES, error="Unsupported runtime", ), Optional("timeout", default=60): And( Use(int), lambda timeout: timeout > 0 ), Optional("env_file_path", default=''): str, Optional("merge_env", default=False): bool, Optional("tags"): {str: str}, Optional("package"): { Optional("include"): [str], Optional("exclude"): [str], Optional("individually", default=False): bool, }, } }, } CONFIG_VALIDATOR = Schema(CONFIG_SCHEMA, ignore_extra_keys=True) PK!E**snakeless/exceptions.pyclass CommandFailure(Exception): pass PK! <snakeless/helpers.pyfrom contextlib import contextmanager from halo import Halo from yaml import load from .constants import CONFIG_VALIDATOR, PROVIDERS from .exceptions import CommandFailure try: from yaml import CLoader as Loader except ImportError: from yaml import Loader @contextmanager def snakeless_spinner(*args, **kwargs): spinner = Halo(*args, **kwargs) spinner.start() try: yield spinner except CommandFailure as exc: spinner.fail(str(exc)) raise except Exception as exc: spinner.fail( spinner.text + "\n" + "Unexpected exception." ) raise finally: spinner.stop() def check_config_existence(root_fs, file_name="snakeless.yml"): return root_fs.exists(file_name) def get_provider(provider_name, config): return PROVIDERS[provider_name](config) def parse_config( root_fs, file_name="snakeless.yml", validator=CONFIG_VALIDATOR ): config_file_data = root_fs.gettext("snakeless.yml") raw_parsed_config = load(config_file_data, Loader=Loader) parse_config = validator.validate(raw_parsed_config) # validate a few more fields mannualy return parse_config PK!Y__**snakeless/providers/__init__.pyfrom .gcloud import GCloudProvider # noqa PK!rccsnakeless/providers/base.pyimport fs class BaseProvider(object): def __init__(self, config): raise NotImplemented def get_func_data(self, func_name, func_data_key, default=None): return self.config["functions"][func_name].get(func_data_key, default) def get_project_data(self, data_key): return self.config["project"][data_key] def get_func_or_project_data(self, func_name, func_data_key, default=None): return self.get_func_data( func_name, func_data_key, default ) or self.get_project_data(func_data_key) def get_env_variables(self, func_name): should_merge_env = self.get_func_data(func_name, "merge_env", False) project_env_file = self.get_project_data("env_file_path") if project_env_file: project_env = self.parse_env_file(project_env_file) else: project_env = {} func_env_file = self.get_func_data(func_name, "env_file_path") if func_env_file: func_env = self.parse_env_file(func_env_file) else: func_env = {} resulting_env = {} if func_env and should_merge_env: resulting_env.update({**project_env, **func_env}) elif func_env: resulting_env = func_env else: resulting_env = project_env return resulting_env def parse_env_file(self, env_file_path): env_variables = {} with fs.open_fs(".") as root_fs: with root_fs.open(env_file_path) as env_file: env_variables = { key: value for key, value in ( line.strip().split("=", maxsplit=2) for line in env_file ) } return env_variables def deploy_function(self, func_name, spinner): raise NotImplemented PK!qsnakeless/providers/gcloud.pyimport json import logging import requests from functools import partial import fs from fs.copy import copy_fs from google.oauth2 import service_account from google.auth.transport.requests import AuthorizedSession from .base import BaseProvider from ..exceptions import CommandFailure logger = logging.getLogger(__name__) logging.getLogger("google").setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("urllib3").setLevel(logging.WARNING) class GCloudApi(object): domain = "https://cloudfunctions.googleapis.com" create_function = { "path": "/v1/projects/{project_id}/locations/{location_id}/functions", "default_method": "POST", } update_function = { "path": "/v1/projects/{project_id}/locations/{location_id}/functions/{function_id}", # noqa "default_method": "PATCH", } upload_code = { "path": ( "/v1/projects/{project_id}/locations/{location_id}/functions:generateUploadUrl" # noqa ), "default_method": "POST", } def call( self, endpoint, path_kwargs={}, session=requests.Session(), **kwargs ): method = kwargs.pop("method", endpoint.get("default_method", "GET")) endpoint_type = endpoint.get("response_type", "json") url = self.domain + endpoint["path"].format(**path_kwargs) r = session.request(method, url, **kwargs) if endpoint_type == "json": return r.json() return r.text class GCloudProvider(BaseProvider): def __init__(self, config): self.config = config service_account_info = None with fs.open_fs(".") as root_fs: service_account_info = json.loads( root_fs.gettext(self.config["project"]["creds"]) ) credentials = service_account.Credentials.from_service_account_info( service_account_info, scopes=["https://www.googleapis.com/auth/cloud-platform"], ) self.session = AuthorizedSession(credentials) self.api = GCloudApi() def generate_resource_function(self, func_name): get_data = partial(self.get_func_or_project_data, func_name) get_func_data = partial(self.get_func_data, func_name) project_id = self.get_project_data("name") location_id = get_data("region") deploy_func_name = get_func_data( "deploy_name", get_func_data("handler") ) env_variables = self.get_env_variables(func_name) source_url = self.upload_func(func_name) resource_function = { "name": "projects/{0}/locations/{1}/functions/{2}".format( project_id, location_id, deploy_func_name ), "description": get_func_data("description", ""), "entryPoint": get_func_data("handler"), "runtime": get_data("runtime"), "timeout": str(get_data("timeout")) + "s", "availableMemoryMb": get_data("memory_size"), "labels": get_func_data("tags", {}), "environmentVariables": env_variables, "sourceUploadUrl": source_url, "httpsTrigger": {"url": get_func_data("path")}, } return resource_function def package_code(self, config, func_name): handler_path = self.config["functions"][func_name]["handler_path"] with fs.open_fs( "./.snakeless/", create=True, writeable=True ) as root_fs: copy_fs(f"{handler_path}", f"zip://.snakeless/{func_name}.zip") return root_fs.getbytes(f"{func_name}.zip") def upload_func(self, func_name): packaged_code = self.package_code(self.config, func_name) response = self.api.call( self.api.upload_code, session=self.session, path_kwargs={ "project_id": self.config["project"]["name"], "location_id": ( self.get_func_data(func_name, "region") or self.config["project"]["region"] ), }, ) upload_url = response["uploadUrl"] r = requests.put( upload_url, headers={ "content-type": "application/zip", "x-goog-content-length-range": "0,104857600", }, data=packaged_code, ) r.raise_for_status() return upload_url def deploy_function( self, func_name, resource_function=None, only_update=False ): resource_function = ( resource_function if resource_function else self.generate_resource_function(func_name) ) deploy_func_name = self.get_func_data( func_name, "deploy_name", self.get_func_data(func_name, "handler") ) response = self.api.call( ( self.api.update_function if only_update else self.api.create_function ), session=self.session, json=resource_function, path_kwargs={ "project_id": self.config["project"]["name"], "location_id": ( self.get_func_data(func_name, "region") or self.config["project"]["region"] ), "function_id": deploy_func_name }, ) if response.get("error"): if response["error"]["status"] == "ALREADY_EXISTS": return self.deploy_function( func_name, only_update=True, resource_function=resource_function, ) logger.error(response) raise CommandFailure("Function deployment has failed") PK!HΫqP}*snakeless-0.1.3.dist-info/entry_points.txtN+I/N.,()*KNI-.r3󸸢z9\ٶHy)V ԂJl.`..PK!Í\--!snakeless-0.1.3.dist-info/LICENSEMIT License Copyright (c) 2018 German Ivanov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.PK!H\TTsnakeless-0.1.3.dist-info/WHEEL 1 0 нR \I$ơ7.ZON `h6oi14m,b4>4ɛpK>X;baP>PK!HW"snakeless-0.1.3.dist-info/METADATAUn6}W> K{K`5H1lF(4eL Oo:b'(Ι33zK/ё2FP&KU5qӱRvNyjBנ h<(Ϧ+ &\{oil̔I#U1bv#j6nѕY#+lv,(ئ"Vq_j吒˖۟t ε$R+L |45?qtp,UU\VE͕|)*bBqkLj |`O@V?o.)4& aZjt+~G"M:7$XϘ|Wh )o\{"eNY-c哛ryWsP q['X,TɇǫmU)ReäG{WtmX1x-Bz;u{=Por붐զ- Ä978`Zq% z*ػpMPT[kbJ{B|$n|޵L[bO'C,[n !A I ʺ-`d] b!<7 ;BCk".Y^YD;72'XX5KgJU&nY_v2JMهOٽ^JiN# ˆ H| 9e^Ӂ֦Zx3L Z! 3=NnH;֔fSy .m.PP^us99DyޗM\mJ\6 M2~EE=o$ʳ?6:d`G7AgŖX~&a$mv}\/{tx$G|*Hݴ"|fN݋m;C+Hy3RP`0*)owYڐF_BÛ$yg3Jrw5OJ&,RڛA!ANb$AG%-V\!m!/e#5sV(<㥇TVaG+