├── .safety-project.ini ├── LICENSE ├── README.md ├── examples ├── app_cache │ ├── accessing_application_cache.py │ ├── avoid_update_cache.py │ ├── clear_cache.py │ └── update_cache.py ├── backup │ ├── with_application.py │ └── with_client.py ├── capture_listener.py ├── client_example.py ├── commit │ ├── with_application.py │ └── with_client.py ├── create_file │ ├── with_application.py │ └── with_client.py ├── creating_config_file.py ├── custom_domain │ ├── with_application.py │ └── with_client.py ├── delete_file │ ├── with_application.py │ └── with_client.py ├── deleting_application │ ├── with_application.py │ └── with_client.py ├── deployments │ ├── create_integration │ │ ├── with_application.py │ │ └── with_client.py │ ├── current_integration │ │ ├── with_application.py │ │ └── with_client.py │ └── deploy_list │ │ ├── with_application.py │ │ └── with_client.py ├── domain_analytics │ ├── with_application.py │ └── with_client.py ├── file_list │ ├── with_application.py │ └── with_client.py ├── get_app_list.py ├── getting_application_status │ ├── with_application.py │ └── with_client.py ├── getting_logs │ ├── with_application.py │ └── with_client.py ├── moving_files │ ├── with_application.py │ └── with_client.py ├── obtaining_app.py ├── read_file │ ├── with_application.py │ └── with_client.py ├── request_listener.py ├── restarting_app │ ├── with_application.py │ └── with_client.py ├── starting_app │ ├── with_application.py │ └── with_client.py ├── stopping_app │ ├── with_application.py │ └── with_client.py └── upload_app │ └── with_client.py ├── poetry.lock ├── pyproject.toml ├── scripts ├── __init__.py └── clear_test_apps.py ├── squarecloud ├── __init__.py ├── _internal │ ├── constants.py │ └── decorators.py ├── app.py ├── client.py ├── data.py ├── errors.py ├── file.py ├── http │ ├── __init__.py │ ├── endpoints.py │ └── http_client.py ├── listeners │ ├── __init__.py │ ├── capture_listener.py │ └── request_listener.py ├── logger.py ├── py.typed └── utils.py └── tests ├── __init__.py ├── conftest.py ├── test_app.py ├── test_app_data.py ├── test_capture_listeners.py ├── test_files.py ├── test_request_listeners.py └── test_upload_app.py /.safety-project.ini: -------------------------------------------------------------------------------- 1 | [project] 2 | id = sdk-api-py 3 | url = /projects/dfc17c5f-6abc-4165-8c26-32954be241e9/findings 4 | name = sdk-api-py 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Robert Nogueira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Square Cloud]: https://squarecloud.app 2 | 3 | [Square Cloud API]: https://docs.squarecloud.app/api-reference/ 4 | 5 | [@allma]: https://github.com/Robert-Nogueira 6 | 7 | 8 |
9 | Square Cloud Banner 10 |
11 | 12 |

squarecloud-api

13 | 14 |

A Python SDK for consuming the Square Cloud API.

15 | 16 | ## Installation 17 | 18 | ````shell 19 | pip install squarecloud-api 20 | ```` 21 | 22 | ## Getting api key 23 | 24 | to get your api key/token just go to the [Square Cloud] website and 25 | register/login, after that go 26 | to `dashboard` > `my account` > `Regenerate API/CLI KEY` and copy the key. 27 | 28 | ## Documentation 29 | Visit our [official documentation](https://docs.squarecloud.app/sdks/py) for more information about how to use this library. 30 | 31 | ## Getting started 32 | 33 | ```python 34 | import asyncio 35 | import squarecloud as square 36 | 37 | client = square.Client('API_KEY') 38 | 39 | async def main(): 40 | status = await client.app_status(app_id='application_id') 41 | print(status) 42 | print(status.ram) 43 | print(status.cpu) 44 | 45 | asyncio.run(main()) 46 | ``` 47 | 48 | ## Contributing 49 | 50 | Feel free to contribute with suggestions or bug reports at our [GitHub repository](https://github.com/squarecloudofc/wrapper-api-py). 51 | 52 | ## Authors 53 | 54 | - [@allma] 55 | -------------------------------------------------------------------------------- /examples/app_cache/accessing_application_cache.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client('API_KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | 11 | # See that since no request was made, the cache is empty 12 | print(app.cache.status) # None 13 | print(app.cache.logs) # None 14 | print(app.cache.backup) # None 15 | 16 | # Now, lets make some requests 17 | await app.status() 18 | await app.logs() 19 | await app.backup() 20 | 21 | # Now the cache is updated 22 | print(app.cache.status) # StatusData(...) 23 | print(app.cache.logs) # LogsData(...) 24 | print(app.cache.backup) # BackupData(...) 25 | 26 | 27 | asyncio.run(example()) 28 | -------------------------------------------------------------------------------- /examples/app_cache/avoid_update_cache.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client('API_KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | 11 | # below we are setting "update_cache" to False, 12 | # so the cache will not be updated when the command is called, 13 | # thus remaining with the old cache 14 | await app.status(update_cache=False) 15 | await app.logs(update_cache=False) 16 | await app.backup(update_cache=False) 17 | 18 | print(app.cache.status) # None 19 | print(app.cache.logs) # None 20 | print(app.cache.backup) # None 21 | 22 | 23 | asyncio.run(example()) 24 | -------------------------------------------------------------------------------- /examples/app_cache/clear_cache.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client('API_KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | 11 | await app.status() 12 | await app.logs() 13 | await app.backup() 14 | 15 | print(app.cache.status) # StatusData(...) 16 | print(app.cache.logs) # LogsData(...) 17 | print(app.cache.backup) # BackupData(...) 18 | 19 | app.cache.clear() # Clear cache 20 | 21 | print(app.cache.status) # None 22 | print(app.cache.logs) # None 23 | print(app.cache.backup) # None 24 | 25 | 26 | asyncio.run(example()) 27 | -------------------------------------------------------------------------------- /examples/app_cache/update_cache.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client('API_KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | 11 | status = await app.status() 12 | logs = await app.logs() 13 | backup = await app.backup() 14 | 15 | app.cache.clear() # Clear cache 16 | 17 | app.cache.update(status, logs, backup) # Update cache 18 | 19 | print(app.cache.status) # StatusData(...) 20 | print(app.cache.logs) # LogsData(...) 21 | print(app.cache.backup) # BackupData(...) 22 | 23 | 24 | asyncio.run(example()) 25 | -------------------------------------------------------------------------------- /examples/backup/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | backup = await app.backup() 11 | print(backup.downloadURL) # https://squarecloud.app/dashboard/backup/f.zip 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/backup/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | backup = await client.backup('application_id') 10 | print(backup.downloadURL) # https://squarecloud.app/dashboard/backup/f.zip 11 | 12 | 13 | asyncio.run(example()) 14 | -------------------------------------------------------------------------------- /examples/capture_listener.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | from squarecloud import Endpoint 5 | 6 | client = square.Client('API_KEY', debug=False) 7 | 8 | 9 | async def example() -> None: 10 | app = await client.app('application_id') 11 | 12 | @app.capture(endpoint=Endpoint.logs()) 13 | async def on_logs_request( 14 | before: square.LogsData, after: square.LogsData 15 | ) -> None: 16 | if after != before: 17 | print(f'New logs!!! {after}') 18 | 19 | await app.logs() # True 20 | await app.logs() # False 21 | 22 | 23 | asyncio.run(example()) 24 | -------------------------------------------------------------------------------- /examples/client_example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | from squarecloud.data import StatusData 5 | 6 | client = square.Client(api_key='API_KEY') 7 | 8 | 9 | async def example() -> None: 10 | app_status: StatusData = await client.app_status('application_id') 11 | print(app_status) 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/commit/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(...) 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app(app_id='application_id') 10 | file = square.File('path/to/you/file.zip') 11 | await app.commit(file=file) 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/commit/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client('API_KEY') 6 | 7 | 8 | async def example() -> None: 9 | file = square.File('path/to/you/file.zip') 10 | await client.commit(file=file, app_id='application_id') 11 | 12 | 13 | asyncio.run(example()) 14 | -------------------------------------------------------------------------------- /examples/create_file/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | 11 | await app.create_file(path='/file.txt', file=square.File('file.txt')) 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/create_file/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.create_app_file( 10 | app_id='application_id', path='/file.txt', file=square.File('file.txt') 11 | ) 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/creating_config_file.py: -------------------------------------------------------------------------------- 1 | from squarecloud.utils import ConfigFile 2 | 3 | # BOT EXAMPLE 4 | ConfigFile( 5 | display_name='an cool name', 6 | description='an cool description', 7 | main='main.py', 8 | memory=256, 9 | version='recommended', # default 'recommended' 10 | auto_restart=False, # default True 11 | ) 12 | 13 | # WEBSITE EXAMPLE 14 | ConfigFile( 15 | display_name='cool website', 16 | description='this is really cool', 17 | main='index.js', 18 | subdomain='cool-subdomain', 19 | start='start this cool website', # if not static it is configurable 20 | memory=512, 21 | version='recommended', # default 'recommended' 22 | auto_restart=False, # default True 23 | ) 24 | 25 | 26 | config = ConfigFile(*...) 27 | 28 | # Saving file 29 | config.save( 30 | 'directory/to/save/' 31 | ) # the path where the file should be saved, default='/' 32 | 33 | # Serializing and Deserialization 34 | config.to_dict() # dict[str, Any] 35 | config.content() # str 36 | 37 | ConfigFile.from_str(...) 38 | ConfigFile.from_dict(...) 39 | 40 | 41 | """ 42 | [REQUIRED] parameters 43 | --------------------- 44 | path: str 45 | display_name: str 46 | main: str 47 | memory: int >= 100 48 | version: Literal['recommended', 'latest'] 49 | 50 | [OPTIONAL] parameters 51 | --------------------- 52 | description: str | None = None 53 | subdomain: str | None = None 54 | start: str | None = None 55 | auto_restart: bool = False, 56 | """ 57 | -------------------------------------------------------------------------------- /examples/custom_domain/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | await app.set_custom_domain('my_custom_domain.example.br') 11 | 12 | 13 | asyncio.run(example()) 14 | -------------------------------------------------------------------------------- /examples/custom_domain/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.set_custom_domain( 10 | 'application_id', 'my_custom_domain.example.br' 11 | ) 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/delete_file/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | 11 | await app.delete_file(path='/file.txt') 12 | 13 | 14 | asyncio.run(example()) 15 | -------------------------------------------------------------------------------- /examples/delete_file/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.delete_app_file(app_id='application_id', path='/file.txt') 10 | 11 | 12 | asyncio.run(example()) 13 | -------------------------------------------------------------------------------- /examples/deleting_application/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | await app.delete() 11 | 12 | 13 | asyncio.run(example()) 14 | -------------------------------------------------------------------------------- /examples/deleting_application/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.delete_app('application_id') 10 | 11 | 12 | asyncio.run(example) 13 | -------------------------------------------------------------------------------- /examples/deployments/create_integration/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | webhook_url = await app.github_integration(access_token='access_token') 11 | print(webhook_url) 12 | 13 | 14 | asyncio.run(example) 15 | -------------------------------------------------------------------------------- /examples/deployments/create_integration/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | webhook_url = await client.github_integration( 10 | 'application_id', access_token='access_token' 11 | ) 12 | print(webhook_url) 13 | 14 | 15 | asyncio.run(example) 16 | -------------------------------------------------------------------------------- /examples/deployments/current_integration/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | webhook_url = await app.current_integration() 11 | print(webhook_url) 12 | 13 | 14 | asyncio.run(example) 15 | -------------------------------------------------------------------------------- /examples/deployments/current_integration/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | webhook_url = await client.current_app_integration( 10 | 'application_id', 11 | ) 12 | print(webhook_url) 13 | 14 | 15 | asyncio.run(example) 16 | -------------------------------------------------------------------------------- /examples/deployments/deploy_list/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | deploys = await app.last_deploys() 11 | print(deploys) # [[DeployData(...), DeployData(...), DeployData(...)]] 12 | 13 | 14 | asyncio.run(example) 15 | -------------------------------------------------------------------------------- /examples/deployments/deploy_list/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | deploys = await client.last_deploys('application_id') 10 | print(deploys) # [[DeployData(...), DeployData(...), DeployData(...)]] 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/domain_analytics/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | analytics = await app.domain_analytics() 11 | print(analytics) # DomainAnalytics(...) 12 | 13 | 14 | asyncio.run(example) 15 | -------------------------------------------------------------------------------- /examples/domain_analytics/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | analytics = await client.last_deploys('application_id') 10 | print(analytics) # DomainAnalytics(...) 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/file_list/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | files_list = await app.files_list(path='/') # list[FileInfo(...)] 11 | 12 | for file in files_list: 13 | print(file.name) # 'main.py' 14 | 15 | print(file.type) # 'directory' or 'file' 16 | 17 | print(file.size) # 2140 18 | 19 | print(file.lastModified) # 1677112835000 20 | 21 | 22 | asyncio.run(example) 23 | -------------------------------------------------------------------------------- /examples/file_list/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | files_list = await client.app_files_list(app_id='application_id', path='/') 10 | 11 | for file in files_list: 12 | print(file.name) # 'main.py' 13 | 14 | print(file.type) # 'directory' or 'file' 15 | 16 | print(file.size) # 2140 17 | 18 | print(file.lastModified) # 1677112835000 19 | 20 | 21 | asyncio.run(example) 22 | -------------------------------------------------------------------------------- /examples/get_app_list.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | apps = await client.all_apps() 10 | print(apps) # list[] 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/getting_application_status/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') # StatusData(...) 10 | status = await app.status() 11 | 12 | print(status.ram) # '70MB' 13 | print(status.cpu) # '5%' 14 | print(status.requests) # 0 15 | print(status.network) # {'total': '0 KB ↑ 0 KB ↓', 'now': '0 KB ↑ 0 KB ↓'} 16 | print(status.running) # True | False 17 | print(status.storage) # '0MB' 18 | 19 | 20 | asyncio.run(example) 21 | -------------------------------------------------------------------------------- /examples/getting_application_status/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | status = await client.app_status('application_id') # StatusData(...) 10 | 11 | print(status.ram) # '70MB' 12 | print(status.cpu) # '5%' 13 | print(status.requests) # 0 14 | print(status.network) # {'total': '0 KB ↑ 0 KB ↓', 'now': '0 KB ↑ 0 KB ↓'} 15 | print(status.running) # True | False 16 | print(status.storage) # '0MB' 17 | 18 | 19 | asyncio.run(example) 20 | -------------------------------------------------------------------------------- /examples/getting_logs/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | logs = await app.logs() 11 | 12 | print(logs) # LogsData(logs='Hello World!') 13 | print(logs.logs) # 'Hello World' 14 | 15 | 16 | asyncio.run(example) 17 | -------------------------------------------------------------------------------- /examples/getting_logs/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | logs = await client.get_logs('application_id') 10 | 11 | print(logs) # LogsData(logs='Hello World!') 12 | print(logs.logs) # 'Hello World' 13 | 14 | 15 | asyncio.run(example) 16 | -------------------------------------------------------------------------------- /examples/moving_files/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | await app.move_file( 11 | origin='path/to/origin/file.py', dest='path/to/destination/file.py' 12 | ) 13 | 14 | 15 | asyncio.run(example) 16 | -------------------------------------------------------------------------------- /examples/moving_files/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.move_app_file( 10 | app_id='application_id', 11 | origin='path/to/origin/file.py', 12 | dest='path/to/destination/file.py', 13 | ) 14 | 15 | 16 | asyncio.run(example) 17 | -------------------------------------------------------------------------------- /examples/obtaining_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application id') 10 | print(app) # 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/read_file/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | file_bytes = await app.read_file(path='main.py') 11 | 12 | print(file_bytes) # b'01101000 01101001' 13 | 14 | 15 | asyncio.run(example) 16 | -------------------------------------------------------------------------------- /examples/read_file/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | file_bytes = await client.read_app_file( 10 | app_id='application_id', path='main.py' 11 | ) 12 | 13 | print(file_bytes) # b'01101000 01101001' 14 | 15 | 16 | asyncio.run(example) 17 | -------------------------------------------------------------------------------- /examples/request_listener.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | from squarecloud import Endpoint 5 | 6 | client = square.Client('API_KEY', debug=False) 7 | 8 | 9 | @client.on_request(endpoint=Endpoint.logs()) 10 | async def on_logs_request(response: square.Response) -> None: 11 | print(1, response) 12 | 13 | 14 | @client.on_request(endpoint=Endpoint.user()) 15 | async def on_user_info_request(response: square.Response) -> None: 16 | print(2, response) 17 | 18 | 19 | async def example() -> None: 20 | await client.get_logs(app_id='application_id') # 1, Response(success) 21 | await client.user() # 2, UserData(...) 22 | await client.user(avoid_listener=True) # the listener is not called 23 | 24 | 25 | asyncio.run(example) 26 | -------------------------------------------------------------------------------- /examples/restarting_app/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | await app.restart() 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/restarting_app/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.restart_app('application_id') 10 | 11 | 12 | asyncio.run(example) 13 | -------------------------------------------------------------------------------- /examples/starting_app/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | await app.start() 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/starting_app/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.start_app('application_id') 10 | 11 | 12 | asyncio.run(example) 13 | -------------------------------------------------------------------------------- /examples/stopping_app/with_application.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | app = await client.app('application_id') 10 | await app.stop() 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /examples/stopping_app/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(api_key='API KEY') 6 | 7 | 8 | async def example() -> None: 9 | await client.stop_app('application_id') 10 | 11 | 12 | asyncio.run(example) 13 | -------------------------------------------------------------------------------- /examples/upload_app/with_client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import squarecloud as square 4 | 5 | client = square.Client(...) 6 | 7 | 8 | async def example() -> None: 9 | file = square.File('path/to/you/file.zip') 10 | await client.upload_app(file=file) 11 | 12 | 13 | asyncio.run(example) 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = 'squarecloud-api' 3 | version = '3.7.2' 4 | description = 'SquareCloud API wrapper' 5 | authors = ['Robert Nogueira '] 6 | repository = 'https://github.com/squarecloudofc/wrapper-api-py' 7 | documentation = 'https://docs.squarecloud.app/sdks/py' 8 | license = 'MIT License' 9 | readme = 'README.md' 10 | packages = [{ include = "squarecloud" }] 11 | 12 | 13 | [tool.poetry.dependencies] 14 | python = '^3.13' 15 | typing-extensions = "^4.12.2" 16 | aiohttp = "^3.11.14" 17 | isort = "^6.0.1" 18 | pre-commit = "^4.2.0" 19 | pytest = "^8.3.5" 20 | pytest-asyncio = "^0.26.0" 21 | pytest-cov = "^6.0.0" 22 | pytest-rerunfailures = "^15.0" 23 | ruff = "^0.11.2" 24 | taskipy = "^1.14.1" 25 | pydantic = "2.9.2" 26 | bandit = {extras = ["toml"], version = "^1.8.3"} 27 | cz-conventional-gitmoji = "^0.7.0" 28 | 29 | [tool.poetry.extras] 30 | pydantic = ["pydantic"] 31 | 32 | [tool.poetry.group.dev.dependencies] 33 | memory-profiler = '^0.61.0' 34 | requests = '^2.31.0' 35 | python-dotenv = "^1.0.1" 36 | safety = "^3.3.1" 37 | vulture = "^2.14" 38 | mypy-extensions = "^1.0.0" 39 | commitizen = "^4.5.1" 40 | bandit = "^1.8.3" 41 | 42 | [build-system] 43 | requires = ['poetry-core'] 44 | build-backend = 'poetry.core.masonry.api' 45 | 46 | [tool.pytest.ini_options] 47 | asyncio_mode = 'auto' 48 | markers = [ 49 | 'app', 50 | 'app_data', 51 | 'listeners', 52 | 'capture_listener', 53 | 'request_listener', 54 | 'files', 55 | 'upload', 56 | ] 57 | 58 | [tool.isort] 59 | profile = 'black' 60 | line_length = 79 61 | 62 | [tool.taskipy.tasks] 63 | lint = 'isort . && blue . && ruff check .' 64 | pre_test = 'task lint' 65 | test = 'pytest -vv -s --reruns 5 --only-rerun TooManyRequests --reruns-delay 90 -x --cov=tests tests' 66 | post_test = 'coverage html' 67 | publish-test = 'poetry publish -r pypi-test --build' 68 | install-test = 'pip install -i https://test.pypi.org/pypi/ --extra-index-url https://pypi.org/simple --upgrade squarecloud-api' 69 | clear-test-apps = 'python -m scripts.clear_test_apps' 70 | 71 | [tool.ruff] 72 | line-length = 79 73 | exclude = ['env', 'tests'] 74 | 75 | [tool.bandit] 76 | exclude_dirs = ["docs", "venv", "migrations", "examples", "tests"] 77 | skips = ["B101", "B311"] 78 | severity_threshold = "low" 79 | confidence_threshold = "low" 80 | recursive = true 81 | quiet = false 82 | 83 | [tool.bandit.tests] 84 | B102 = "exec_used" # Use of 'exec' detected 85 | B103 = "set_bad_file_permissions" # Insecure file permissions 86 | B104 = "hardcoded_bind_all_interfaces" # Binding to all interfaces 87 | B105 = "hardcoded_password_string" # Hardcoded password in a string 88 | B106 = "hardcoded_password_funcarg" # Hardcoded password in function argument 89 | B107 = "hardcoded_password_default" # Hardcoded password in a default argument 90 | B108 = "hardcoded_tmp_directory" # Use of hardcoded temporary directory 91 | B109 = "password_config_option_not_marked_secret" # Password config not marked as secret 92 | B110 = "try_except_pass" # 'try-except-pass' pattern detected 93 | B111 = "execute_with_run_as_root_equals_true" # Executing with 'run_as_root=True' 94 | B112 = "try_except_continue" # 'try-except-continue' pattern detected 95 | B113 = "request_without_timeout" # HTTP request without a timeout 96 | 97 | # Security vulnerabilities in web applications 98 | B201 = "flask_debug_true" # Flask app running with 'debug=True' 99 | B202 = "tarfile_unsafe_members" # Tarfile extract with unsafe members 100 | 101 | # Cryptography-related issues 102 | B324 = "hashlib" # Weak or insecure hash function 103 | 104 | # SSL/TLS issues 105 | B501 = "request_with_no_cert_validation" # Requests without certificate validation 106 | B502 = "ssl_with_bad_version" # Use of outdated SSL versions 107 | B503 = "ssl_with_bad_defaults" # Insecure SSL defaults 108 | B504 = "ssl_with_no_version" # SSL/TLS without specifying a version 109 | B505 = "weak_cryptographic_key" # Weak cryptographic key detected 110 | 111 | # Serialization & data handling 112 | B506 = "yaml_load" # Use of unsafe 'yaml.load' 113 | B507 = "ssh_no_host_key_verification" # SSH without host key verification 114 | B508 = "snmp_insecure_version" # Insecure SNMP version used 115 | B509 = "snmp_weak_cryptography" # Weak SNMP cryptography used 116 | 117 | # Process execution risks 118 | B601 = "paramiko_calls" # Use of Paramiko library detected 119 | B602 = "subprocess_popen_with_shell_equals_true" # Popen with 'shell=True' 120 | B603 = "subprocess_without_shell_equals_true" # Popen without 'shell=True' 121 | B604 = "any_other_function_with_shell_equals_true" # Dangerous shell execution function 122 | B605 = "start_process_with_a_shell" # Starting a process with a shell 123 | B606 = "start_process_with_no_shell" # Process started without shell 124 | B607 = "start_process_with_partial_path" # Process started with a partial path 125 | 126 | # Database security 127 | B608 = "hardcoded_sql_expressions" # Hardcoded SQL expressions detected 128 | B609 = "linux_commands_wildcard_injection" # Linux command injection via wildcards 129 | B610 = "django_extra_used" # Use of Django's 'extra()' method 130 | B611 = "django_rawsql_used" # Use of raw SQL in Django 131 | B612 = "logging_config_insecure_listen" # Insecure logging configuration 132 | 133 | # Miscellaneous security issues 134 | B613 = "trojansource" # Trojan Source attack patterns detected 135 | B614 = "pytorch_load" # Use of 'torch.load', which can be unsafe 136 | 137 | # Template security risks 138 | B701 = "jinja2_autoescape_false" # Jinja2 autoescape disabled 139 | B702 = "use_of_mako_templates" # Use of Mako templates detected 140 | B703 = "django_mark_safe" # Use of Django's 'mark_safe' 141 | B704 = "markupsafe_markup_xss" # Potential XSS via MarkupSafe 142 | 143 | [tool.commitizen] 144 | name = "cz_gitmoji" 145 | 146 | [tool.ruff.lint.per-file-ignores] 147 | '__init__.py' = ['F401'] -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from datetime import datetime 3 | from typing import Any, Callable 4 | 5 | from rich.status import Status 6 | 7 | 8 | def run_async_script(func: Callable) -> Callable: 9 | def wrapper(*args, **kwargs) -> Any: 10 | with Status(f'Running {func.__name__}', spinner='point'): 11 | before = datetime.now() 12 | result = asyncio.run(func(*args, **kwargs)) 13 | after = datetime.now() 14 | print( 15 | f'\u2713 Script ran successfully in ' 16 | f'{(after - before).seconds} seconds!' 17 | ) 18 | return result 19 | 20 | return wrapper 21 | -------------------------------------------------------------------------------- /scripts/clear_test_apps.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | import squarecloud 6 | from scripts import run_async_script 7 | 8 | load_dotenv() 9 | client = squarecloud.Client(os.getenv('KEY')) 10 | 11 | 12 | @run_async_script 13 | async def delete_test_apps() -> None: 14 | for app in await client.all_apps(): 15 | if '_test' in app.name: 16 | await app.delete() 17 | print(f'\U0001f5d1 Deleted app {app.name}, with id {app.id}...') 18 | 19 | 20 | if __name__ == '__main__': 21 | delete_test_apps() 22 | -------------------------------------------------------------------------------- /squarecloud/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import errors, utils 4 | from .app import Application 5 | from .client import Client 6 | from .data import ( 7 | AppData, 8 | Backup, 9 | BackupInfo, 10 | DeployData, 11 | DNSRecord, 12 | DomainAnalytics, 13 | FileInfo, 14 | LogsData, 15 | PlanData, 16 | ResumedStatus, 17 | StatusData, 18 | UploadData, 19 | UserData, 20 | ) 21 | from .file import File 22 | from .http.endpoints import Endpoint 23 | from .http.http_client import Response 24 | 25 | __all__ = [ 26 | 'Application', 27 | 'Client', 28 | 'File', 29 | 'Endpoint', 30 | 'Response', 31 | 'AppData', 32 | 'Backup', 33 | 'BackupInfo', 34 | 'DeployData', 35 | 'DNSRecord', 36 | 'DomainAnalytics', 37 | 'FileInfo', 38 | 'LogsData', 39 | 'PlanData', 40 | 'ResumedStatus', 41 | 'StatusData', 42 | 'UploadData', 43 | 'UserData', 44 | 'errors', 45 | 'utils', 46 | ] 47 | 48 | __version__ = '3.7.2' 49 | -------------------------------------------------------------------------------- /squarecloud/_internal/constants.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec 2 | 3 | USING_PYDANTIC = bool(find_spec('pydantic')) 4 | -------------------------------------------------------------------------------- /squarecloud/_internal/decorators.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, TypeVar 2 | 3 | from .constants import USING_PYDANTIC 4 | 5 | if USING_PYDANTIC: 6 | from pydantic import ConfigDict, validate_call 7 | 8 | F = TypeVar('F', bound=Callable[..., Any]) 9 | 10 | 11 | def validate(func: F) -> Callable[..., Any] | F: 12 | if USING_PYDANTIC: 13 | return validate_call(config=ConfigDict(arbitrary_types_allowed=True))( 14 | func 15 | ) 16 | return func 17 | -------------------------------------------------------------------------------- /squarecloud/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from functools import wraps 4 | from io import BytesIO 5 | from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar 6 | 7 | from squarecloud import errors 8 | 9 | from ._internal.decorators import validate 10 | from .data import ( 11 | AppData, 12 | Backup, 13 | BackupInfo, 14 | DeployData, 15 | DNSRecord, 16 | DomainAnalytics, 17 | FileInfo, 18 | LogsData, 19 | StatusData, 20 | ) 21 | from .file import File 22 | from .http import Endpoint, HTTPClient, Response 23 | from .listeners import Listener, ListenerConfig 24 | from .listeners.capture_listener import CaptureListenerManager 25 | 26 | # avoid circular imports 27 | if TYPE_CHECKING: 28 | from .client import Client 29 | 30 | T = TypeVar('T') 31 | 32 | AsyncCallable = TypeVar( 33 | 'AsyncCallable', bound=Callable[..., Coroutine[Any, Any, T]] 34 | ) 35 | 36 | 37 | class AppCache: 38 | __slots__ = ( 39 | '_status', 40 | '_logs', 41 | '_backup', 42 | '_app_data', 43 | ) 44 | 45 | def __init__(self) -> None: 46 | """ 47 | The `__init__` function is called when the class is instantiated. 48 | It sets up the instance of the class, and defines all of its 49 | attributes. 50 | 51 | 52 | :return: None 53 | """ 54 | self._status: StatusData | None = None 55 | self._logs: LogsData | None = None 56 | self._backup: Backup | None = None 57 | self._app_data: AppData | None = None 58 | 59 | @property 60 | def status(self) -> StatusData: 61 | """ 62 | The status method is a property that returns the cached StatusData of 63 | the application. 64 | 65 | :return: A StatusData object. 66 | :rtype: StatusData 67 | """ 68 | return self._status 69 | 70 | @property 71 | def logs(self) -> LogsData: 72 | """ 73 | The logs method is a property that returns the cached LogsData of 74 | the application. 75 | 76 | :return: The logs of your application 77 | :rtype: LogsData 78 | """ 79 | return self._logs 80 | 81 | @property 82 | def backup(self) -> Backup: 83 | """ 84 | The backup method is a property that returns the cached Backup of 85 | the application. 86 | 87 | :return: The value of the _backup attribute 88 | :rtype: Backup 89 | """ 90 | return self._backup 91 | 92 | @property 93 | def app_data(self) -> AppData: 94 | """ 95 | The app_data method is a property that returns the cached AppData 96 | object 97 | 98 | :return: The data from the app_data 99 | :rtype: AppData 100 | """ 101 | return self._app_data 102 | 103 | def clear(self) -> None: 104 | """ 105 | The clear method is used to clear the status, logs, backup and data 106 | variables. 107 | 108 | :param self: Refer to the class instance 109 | :return: None 110 | """ 111 | self._status = None 112 | self._logs = None 113 | self._backup = None 114 | self._app_data = None 115 | 116 | def update(self, *args) -> None: 117 | """ 118 | The update method is used to update the data of a given instance. 119 | It takes in an arbitrary number of arguments, and updates the 120 | corresponding data if it is one of the following types: 121 | StatusData, LogsData, Backup or AppData. 122 | If any other type is provided as an argument to this function, 123 | a SquareException will be raised. 124 | 125 | :param args: Pass a variable number of arguments to a function 126 | :return: None 127 | """ 128 | for arg in args: 129 | if isinstance(arg, StatusData): 130 | self._status = arg 131 | elif isinstance(arg, LogsData): 132 | self._logs = arg 133 | elif isinstance(arg, Backup): 134 | self._backup = arg 135 | elif isinstance(arg, AppData): 136 | self._app_data = arg 137 | else: 138 | types: list = [ 139 | i.__name__ 140 | for i in [ 141 | StatusData, 142 | LogsData, 143 | Backup, 144 | AppData, 145 | ] 146 | ] 147 | raise errors.SquareException( 148 | f'you must provide stats of the following types:\n{types}' 149 | ) 150 | 151 | 152 | class Application(CaptureListenerManager): 153 | """Represents an application""" 154 | 155 | __slots__ = [ 156 | '_client', 157 | '_http', 158 | '_listener', 159 | '_data', 160 | 'cache', 161 | '_id', 162 | '_name', 163 | '_desc', 164 | '_domain', 165 | '_custom', 166 | '_ram', 167 | '_lang', 168 | '_cluster', 169 | 'always_avoid_listeners', 170 | ] 171 | 172 | def __init__( 173 | self, 174 | client: Client, 175 | http: HTTPClient, 176 | id: str, 177 | name: str, 178 | ram: int, 179 | lang: str, 180 | cluster: str, 181 | domain: str | None, 182 | custom: str | None, 183 | desc: str | None = None, 184 | ) -> None: 185 | """ 186 | The `__init__` method is called when the class is instantiated. 187 | It sets up all the attributes that are passed in as arguments, 188 | and does any other initialization your class needs before It's ready 189 | for use. 190 | 191 | 192 | :param client: Store a reference to the client that created 193 | this app. 194 | :param http: Store a reference to the HTTPClient 195 | :param id: The application id 196 | :param name: The tag of the app 197 | :param ram: The amount of ram that is allocated 198 | :param lang: The programming language of the app 199 | :param cluster: The cluster that the app is hosted on 200 | :param desc: Define the description of the app 201 | :param domain: Define the domain of the app 202 | :param custom: Define the custom domain of the app 203 | 204 | :return: None 205 | """ 206 | self._id: str = id 207 | self._name: str = name 208 | self._domain: str | None = domain 209 | self._custom: str | None = custom 210 | self._desc: str | None = desc 211 | self._ram: int = ram 212 | self._lang: str = lang 213 | self._cluster: str = cluster 214 | self._client: Client = client 215 | self._http: HTTPClient = http 216 | self._listener: CaptureListenerManager = CaptureListenerManager() 217 | self.cache: AppCache = AppCache() 218 | self.always_avoid_listeners: bool = False 219 | super().__init__() 220 | 221 | def __repr__(self) -> str: 222 | """ 223 | The `__repr__` method is used to create a string representation of an 224 | object. 225 | This is useful for debugging, logging and other instances where you 226 | would want a string representation of the object. 227 | The __repr__ function should return a string that would make sense to 228 | someone looking at the results in the interactive interpreter. 229 | 230 | :return: The class name, tag and id of the element 231 | :rtype: str 232 | """ 233 | return f'<{self.__class__.__name__} tag={self.name} id={self.id}>' 234 | 235 | @property 236 | def client(self) -> Client: 237 | """ 238 | The client method is a property that returns the client object. 239 | 240 | :return: The client instance 241 | :rtype: Client 242 | """ 243 | return self._client 244 | 245 | @property 246 | def id(self) -> str: 247 | """ 248 | The id function is a property that returns 249 | the id of the application. 250 | 251 | :return: The id of the application 252 | :rtype: str 253 | """ 254 | return self._id 255 | 256 | @property 257 | def name(self) -> str: 258 | """ 259 | The tag function is a property that returns the application tag. 260 | 261 | :return: The tag of the application 262 | :rtype: str 263 | """ 264 | return self._name 265 | 266 | @property 267 | def desc(self) -> str | None: 268 | """ 269 | The desc function is a property that returns the description of 270 | the application. 271 | 272 | :return: The description of the application 273 | :rtype: str | None 274 | """ 275 | return self._desc 276 | 277 | @property 278 | def ram(self) -> int: 279 | """ 280 | The ram function is a property that returns 281 | the amount of ram allocated to the application 282 | 283 | :return: The application ram 284 | :rtype: PositiveInt 285 | """ 286 | return self._ram 287 | 288 | @property 289 | def lang(self) -> str: 290 | """ 291 | The lang function is a property that returns the application's 292 | programing language. 293 | 294 | :return: The application's programing language 295 | :rtype: str 296 | """ 297 | return self._lang 298 | 299 | @property 300 | def cluster(self) -> str: 301 | """ 302 | The cluster function is a property that returns the 303 | cluster that the application is 304 | running on. 305 | 306 | 307 | :return: The cluster that the application is running 308 | :rtype: str 309 | """ 310 | return self._cluster 311 | 312 | @property 313 | def domain(self) -> str | None: 314 | """ 315 | The domain function is a property that returns the 316 | application's domain. 317 | 318 | 319 | :return: The application's domain 320 | :rtype: str | None 321 | """ 322 | return self._domain 323 | 324 | @property 325 | def custom(self) -> str | None: 326 | """ 327 | The custom function is a property that returns the 328 | application's custom domain. 329 | 330 | 331 | :return: The application's domain 332 | :rtype: str | None 333 | """ 334 | return self._custom 335 | 336 | @staticmethod 337 | def _notify_listener( 338 | endpoint: Endpoint, 339 | ) -> Callable[[AsyncCallable], AsyncCallable]: 340 | """ 341 | The _notify_listener function is a decorator that call a listener after 342 | the decorated coroutine is called 343 | 344 | :param endpoint: the endpoint for witch the listener will fetch 345 | :return: a callable 346 | """ 347 | 348 | def wrapper(func: AsyncCallable) -> AsyncCallable: 349 | @wraps(func) 350 | async def decorator(self: Application, *args, **kwargs) -> Any: 351 | result = await func(self, *args, **kwargs) 352 | avoid_listener = kwargs.pop('avoid_listener', False) 353 | if not (avoid_listener or self.always_avoid_listeners): 354 | await self.notify( 355 | endpoint=endpoint, 356 | before=self.cache.app_data, 357 | after=result, 358 | extra_value=kwargs.get('extra'), 359 | ) 360 | return result 361 | 362 | return decorator 363 | 364 | return wrapper 365 | 366 | @staticmethod 367 | def _update_cache(func: AsyncCallable) -> AsyncCallable: 368 | """ 369 | This is a decorator checks whether the kwargs `update_cache` in the 370 | decorated coroutine is not False, and updates the application cache 371 | 372 | :param func: 373 | :return: a callable 374 | """ 375 | 376 | @wraps(func) 377 | async def wrapper(self: Application, *args, **kwargs) -> T: 378 | update_cache = kwargs.pop('update_cache', True) 379 | result = await func(self, *args, **kwargs) 380 | if update_cache: 381 | self.cache.update(result) 382 | return result 383 | 384 | return wrapper 385 | 386 | def capture(self, endpoint: Endpoint, **kwargs) -> Callable: 387 | """ 388 | The capture function is a decorator that can be used to add a callable 389 | to be called when a request is made to the specified endpoint. 390 | 391 | :param self: Refer to the class instance. 392 | :param endpoint: Endpoint: Specify which endpoint the function will be 393 | called on 394 | :return: A decorator 395 | :rtype: Callable 396 | """ 397 | 398 | def wrapper( 399 | call: Callable[[Endpoint, Endpoint], Any], 400 | ) -> Callable[[Endpoint, Endpoint], Any]: 401 | """ 402 | The wrapper function is a decorator that takes in the endpoint as 403 | an argument. 404 | It then checks if the endpoint is allowed, and if it isn't, raises 405 | a SquareException. 406 | If there's no capture_listener for that endpoint yet, it adds one 407 | with the function passed to wrapper(). 408 | Otherwise, it raises another SquareException. 409 | 410 | :param call: Pass the function to be wrapped 411 | :return: The wrapper function itself 412 | :rtype: None 413 | :raises InvalidListener: Raised if the endpoint is already 414 | registered 415 | """ 416 | for key, value in kwargs.items(): 417 | if key not in ListenerConfig.__annotations__: 418 | raise ValueError( 419 | f'Invalid listener configuration: "{key}={value}"' 420 | ) 421 | config = ListenerConfig(**kwargs) 422 | listener = Listener( 423 | app=self, 424 | client=self.client, 425 | endpoint=endpoint, 426 | callback=call, 427 | config=config, 428 | ) 429 | self.include_listener(listener) 430 | return call 431 | 432 | return wrapper 433 | 434 | @_update_cache 435 | @_notify_listener(Endpoint.logs()) 436 | async def logs(self, *_args, **_kwargs) -> LogsData: 437 | """ 438 | The logs method is used to get the application's logs. 439 | 440 | :param self: Refer to the class instance 441 | :return: A LogsData object 442 | :rtype: LogsData 443 | """ 444 | logs: LogsData = await self.client.get_logs(self.id) 445 | return logs 446 | 447 | @_update_cache 448 | @_notify_listener(Endpoint.app_status()) 449 | async def status(self, *_args, **_kwargs) -> StatusData: 450 | """ 451 | The status function returns the status of an application. 452 | 453 | :param self: Refer to the class instance 454 | :return: A StatusData object 455 | :rtype: StatusData 456 | """ 457 | status: StatusData = await self.client.app_status(self.id) 458 | return status 459 | 460 | @_update_cache 461 | @_notify_listener(Endpoint.backup()) 462 | async def backup(self, *_args, **_kwargs) -> Backup: 463 | """ 464 | The backup function is used to create a backup of the application. 465 | 466 | :param self: Refer to the class instance 467 | :return: A Backup object 468 | :rtype: Backup 469 | """ 470 | backup: Backup = await self.client.backup(self.id) 471 | return backup 472 | 473 | async def start(self) -> Response: 474 | """ 475 | The start function starts the application. 476 | 477 | :param self: Refer to the class instance 478 | :return: A Response object 479 | :rtype: Response 480 | """ 481 | response: Response = await self.client.start_app( 482 | self.id, avoid_listener=True 483 | ) 484 | return response 485 | 486 | async def stop(self) -> Response: 487 | """ 488 | The stop function stops the application. 489 | 490 | :param self: Refer to the class instance 491 | :return: A Response object 492 | :rtype: Response 493 | """ 494 | response: Response = await self.client.stop_app( 495 | self.id, avoid_listener=True 496 | ) 497 | return response 498 | 499 | async def restart(self) -> Response: 500 | """ 501 | The restart function restarts the application. 502 | 503 | :param self: Refer to the class instance 504 | :return: The Response object 505 | :rtype: Response 506 | """ 507 | response: Response = await self.client.restart_app( 508 | self.id, avoid_listener=True 509 | ) 510 | return response 511 | 512 | async def delete(self) -> Response: 513 | """ 514 | The delete function deletes the application. 515 | 516 | :param self: Refer to the class instance 517 | :return: A Response object 518 | :rtype: Response 519 | """ 520 | response: Response = await self.client.delete_app( 521 | self.id, avoid_listener=True 522 | ) 523 | return response 524 | 525 | @validate 526 | async def commit(self, file: File) -> Response: 527 | """ 528 | The commit function is used to commit the application. 529 | 530 | 531 | :param self: Refer to the class instance 532 | :param file: File: The squarecloud.File to be committed 533 | :return: A Response object 534 | :rtype: Response 535 | """ 536 | response: Response = await self.client.commit( 537 | self.id, file=file, avoid_listener=True 538 | ) 539 | return response 540 | 541 | @validate 542 | async def files_list(self, path: str) -> list[FileInfo]: 543 | """ 544 | The files_list function returns a list of files and folders in the 545 | specified directory. 546 | 547 | :param self: Refer to the class instance 548 | :param path: str: Specify the path of the file to be listed 549 | :return: A list of FileInfo objects. 550 | :rtype: list[FileInfo] 551 | """ 552 | response: list[FileInfo] = await self.client.app_files_list( 553 | self.id, 554 | path, 555 | avoid_listener=True, 556 | ) 557 | return response 558 | 559 | @validate 560 | async def read_file(self, path: str) -> BytesIO: 561 | """ 562 | The read_file function reads the contents of a file from an app. 563 | 564 | :param self: Refer to the class instance 565 | :param path: str: Specify the path of the file to be read 566 | :return: A BytesIO object 567 | :rtype: BytesIO 568 | """ 569 | response: BytesIO = await self.client.read_app_file( 570 | self.id, path, avoid_listener=True 571 | ) 572 | return response 573 | 574 | @validate 575 | async def create_file(self, file: File, path: str) -> Response: 576 | """ 577 | The create_file function creates a file in the specified path. 578 | 579 | :param self: Refer to the class instance 580 | :param file: File: Specify the file that is to be uploaded 581 | :param path: str: Specify the path of the file to be created 582 | :return: A Response object 583 | :rtype: Response 584 | """ 585 | response: Response = await self.client.create_app_file( 586 | self.id, 587 | file, 588 | path, 589 | avoid_listener=True, 590 | ) 591 | return response 592 | 593 | @validate 594 | async def delete_file(self, path: str) -> Response: 595 | """ 596 | The delete_file function deletes a file from the app. 597 | 598 | :param self: Refer to the class instance 599 | :param path: str: Specify the path of the file to be deleted 600 | :return: A Response object 601 | :rtype: Response 602 | """ 603 | response: Response = await self.client.delete_app_file( 604 | self.id, path, avoid_listener=True 605 | ) 606 | return response 607 | 608 | async def last_deploys(self) -> list[list[DeployData]]: 609 | """ 610 | The last_deploys function returns a list of the last deploys for this 611 | application. 612 | 613 | :param self: Represent the instance of the class 614 | :param: Pass in keyword arguments as a dictionary 615 | :return: A list of DeployData objects 616 | """ 617 | response: list[list[DeployData]] = await self.client.last_deploys( 618 | self.id, 619 | avoid_listener=True, 620 | ) 621 | return response 622 | 623 | @validate 624 | async def github_integration(self, access_token: str) -> str: 625 | """ 626 | The create_github_integration function returns a webhook to integrate 627 | with a GitHub repository. 628 | 629 | :param self: Access the properties of the class 630 | :param access_token: str: Authenticate the user with GitHub 631 | :return: A string containing the webhook url 632 | """ 633 | webhook: str = await self.client.github_integration( 634 | self.id, 635 | access_token, 636 | avoid_listener=True, 637 | ) 638 | return webhook 639 | 640 | async def domain_analytics(self) -> DomainAnalytics: 641 | analytics: DomainAnalytics = await self.client.domain_analytics( 642 | self.id, avoid_listener=True 643 | ) 644 | return analytics # TODO: 645 | 646 | async def set_custom_domain(self, custom_domain: str) -> Response: 647 | response: Response = await self.client.set_custom_domain( 648 | self.id, custom_domain, avoid_listener=True 649 | ) 650 | return response 651 | 652 | async def all_backups(self) -> Response: 653 | backups: list[BackupInfo] = await self.client.all_app_backups(self.id) 654 | return backups 655 | 656 | @validate 657 | async def move_file(self, origin: str, dest: str) -> Response: 658 | return await self.client.move_app_file(self.id, origin, dest) 659 | 660 | async def current_integration(self) -> Response: 661 | return await self.client.current_app_integration(self.id) 662 | 663 | @_notify_listener(Endpoint.dns_records()) 664 | async def dns_records(self) -> list[DNSRecord]: 665 | return await self.client.dns_records(self.id) 666 | -------------------------------------------------------------------------------- /squarecloud/client.py: -------------------------------------------------------------------------------- 1 | """This module is a wrapper for using the SquareCloud API""" 2 | 3 | from __future__ import annotations 4 | 5 | from functools import wraps 6 | from io import BytesIO 7 | from typing import Any, Callable, Literal, ParamSpec, TextIO, TypeVar 8 | 9 | from typing_extensions import deprecated 10 | 11 | from ._internal.decorators import validate 12 | from .app import Application 13 | from .data import ( 14 | AppData, 15 | Backup, 16 | BackupInfo, 17 | DeployData, 18 | DNSRecord, 19 | DomainAnalytics, 20 | FileInfo, 21 | LogsData, 22 | ResumedStatus, 23 | StatusData, 24 | UploadData, 25 | UserData, 26 | ) 27 | from .errors import ApplicationNotFound, InvalidFile, SquareException 28 | from .file import File 29 | from .http import HTTPClient, Response 30 | from .http.endpoints import Endpoint 31 | from .listeners import Listener, ListenerConfig 32 | from .listeners.request_listener import RequestListenerManager 33 | from .logger import logger 34 | 35 | P = ParamSpec("P") 36 | R = TypeVar("R") 37 | 38 | 39 | @deprecated( 40 | "create_config_file is deprecated, " 41 | "use squarecloud.utils.ConfigFile instead." 42 | ) 43 | def create_config_file( 44 | path: str, 45 | display_name: str, 46 | main: str, 47 | memory: int, 48 | version: Literal["recommended", "latest"] = "recommended", 49 | description: str | None = None, 50 | subdomain: str | None = None, 51 | start: str | None = None, 52 | auto_restart: bool = False, 53 | **kwargs, 54 | ) -> TextIO | str: 55 | """ 56 | The create_config_file function creates a squarecloud.app file in the 57 | specified path, with the given parameters. 58 | The function takes in 8 arguments: 59 | 60 | :param path: str: Specify the path to the folder where you want to create 61 | your config file 62 | :param display_name: str: Set the display name of your app 63 | :param main: str: Specify the file that will be executed when the app 64 | is started 65 | :param memory: int: Set the memory of the app 66 | :param version: Literal['recommended', 'latest']: Ensure that the version 67 | is either 'recommended' or 'latest'. 68 | :param description: str | None: Specify a description for the app 69 | :param subdomain: str | None: Specify the subdomain of your app 70 | :param start: str | None: Specify the command that should be run when the 71 | application starts 72 | :param auto_restart: bool | None: Determine if the app should restart 73 | automatically after a crash 74 | :return: File content 75 | :rtype: str 76 | """ 77 | content: str = "" 78 | optionals: dict[str, Any] = { 79 | "DISPLAY_NAME": display_name, 80 | "MAIN": main, 81 | "MEMORY": memory, 82 | "VERSION": version, 83 | "DESCRIPTION": description, 84 | "SUBDOMAIN": subdomain, 85 | "START": start, 86 | "AUTORESTART": auto_restart, 87 | } 88 | for key, value in optionals.items(): 89 | if value: 90 | string: str = f"{key}={value}\n" 91 | content += string 92 | if kwargs.get("save", True): 93 | with open(f"./{path}/squarecloud.app", "w", encoding="utf-8") as file: 94 | file.write(content) 95 | return file 96 | return content 97 | 98 | 99 | class Client(RequestListenerManager): 100 | """A client for interacting with the SquareCloud API.""" 101 | 102 | def __init__( 103 | self, 104 | api_key: str, 105 | log_level: Literal[ 106 | "DEBUG", 107 | "INFO", 108 | "WARNING", 109 | "ERROR", 110 | "CRITICAL", 111 | ] = "INFO", 112 | ) -> None: 113 | """ 114 | The __init__ function is called when the class is instantiated. 115 | It sets up the instance of the class, and defines all of its 116 | attributes. 117 | 118 | 119 | :param self: Refer to the class instance 120 | :param api_key: str: Your API key, get in: 121 | https://squarecloud.app/dashboard/me 122 | :param debug: bool: Set the logging level to debug 123 | :return: None 124 | """ 125 | self.log_level = log_level 126 | self._api_key = api_key 127 | self._http = HTTPClient(api_key=api_key) 128 | self.logger = logger 129 | logger.setLevel(log_level) 130 | super().__init__() 131 | 132 | @property 133 | def api_key(self) -> str: 134 | """ 135 | Returns the api key for the client. 136 | 137 | :return: The api key 138 | :rtype: str 139 | """ 140 | return self._api_key 141 | 142 | def on_request(self, endpoint: Endpoint, **kwargs) -> Callable: 143 | """ 144 | The on_request function is a decorator that allows you to register a 145 | function as an endpoint listener. 146 | 147 | :param endpoint: Endpoint: Specify the endpoint that will be used to 148 | capture the request 149 | :return: A wrapper function 150 | """ 151 | 152 | def wrapper(func: Callable) -> None: 153 | """ 154 | The wrapper function is a decorator that wraps the function passed 155 | to it. 156 | It takes in a function, and returns another function. The wrapper 157 | will call 158 | the wrapped function with all of its arguments, and then do 159 | something extra 160 | with the result. 161 | 162 | :param func: Callable: Specify the type of the parameter 163 | :return: The function itself, if the endpoint is not already 164 | registered 165 | :raises SquarecloudException: Raised if the endpoint is already 166 | registered 167 | """ 168 | for key, value in kwargs.items(): 169 | if key not in ListenerConfig.__annotations__: 170 | raise ValueError( 171 | f'Invalid listener configuration: "{key}={value}"' 172 | ) 173 | config = ListenerConfig(**kwargs) 174 | listener = Listener( 175 | endpoint=endpoint, callback=func, client=self, config=config 176 | ) 177 | self.include_listener(listener) 178 | 179 | return wrapper 180 | 181 | @staticmethod 182 | def _notify_listener(endpoint: Endpoint) -> Callable: 183 | """ 184 | The _notify_listener function is a decorator that call a listener after 185 | the decorated coroutine is called 186 | 187 | :param endpoint: the endpoint for witch the listener will fetch 188 | :return: a callable 189 | """ 190 | 191 | def wrapper(func: Callable[P, R]) -> Callable[P, R]: 192 | @wraps(func) 193 | async def decorator( 194 | self: Client, *args: P.args, **kwargs: P.kwargs 195 | ) -> R: 196 | # result: Any 197 | response: Response 198 | result = await func(self, *args, **kwargs) 199 | response = self._http.last_response 200 | if kwargs.get("avoid_listener", False): 201 | return result 202 | await self.notify( 203 | endpoint=endpoint, 204 | response=response, 205 | extra_value=kwargs.get("extra"), 206 | ) 207 | return result 208 | 209 | return decorator 210 | 211 | return wrapper 212 | 213 | @_notify_listener(Endpoint.user()) 214 | async def user(self, **_kwargs) -> UserData: 215 | """ 216 | This method is used to get your information. 217 | 218 | :param _kwargs: Keyword arguments 219 | :return: A UserData object 220 | :rtype: UserData 221 | 222 | :raises BadRequestError: Raised when the request status code is 400 223 | :raises AuthenticationFailure: Raised when the request status 224 | code is 401 225 | :raises TooManyRequestsError: Raised when the request status 226 | code is 429 227 | """ 228 | response: Response = await self._http.fetch_user_info() 229 | payload: dict[str, Any] = response.response 230 | return UserData(**payload["user"]) 231 | 232 | @_notify_listener(Endpoint.logs()) 233 | async def get_logs(self, app_id: str, **_kwargs) -> LogsData: 234 | """ 235 | The get_logs method is used to get logs for an application. 236 | 237 | :param app_id: Specify the application by id 238 | :param _kwargs: Keyword arguments 239 | :return: A LogsData object 240 | :rtype: LogsData 241 | 242 | :raises NotFoundError: Raised when the request status code is 404 243 | :raises BadRequestError: Raised when the request status code is 400 244 | :raises AuthenticationFailure: Raised when the request status 245 | code is 401 246 | :raises TooManyRequestsError: Raised when the request status 247 | code is 429 248 | """ 249 | response: Response = await self._http.fetch_logs(app_id) 250 | payload: dict[str, Any] | None = response.response 251 | if not payload: 252 | logs_data: LogsData = LogsData() 253 | else: 254 | logs_data: LogsData = LogsData(**payload) 255 | 256 | return logs_data 257 | 258 | @validate 259 | @_notify_listener(Endpoint.app_status()) 260 | async def app_status(self, app_id: str, **_kwargs) -> StatusData: 261 | """ 262 | The app_status method is used to get the status of an application. 263 | 264 | :param app_id: Specify the application by id 265 | :param _kwargs: Keyword arguments 266 | :return: A StatusData object 267 | :rtype: StatusData 268 | 269 | :raises NotFoundError: Raised when the request status code is 404 270 | :raises BadRequestError: Raised when the request status code is 400 271 | :raises AuthenticationFailure: Raised when the request status 272 | code is 401 273 | :raises TooManyRequestsError: Raised when the request status 274 | code is 429 275 | """ 276 | response: Response = await self._http.fetch_app_status(app_id) 277 | payload: dict[str, Any] = response.response 278 | return StatusData(**payload) 279 | 280 | @validate 281 | @_notify_listener(Endpoint.start()) 282 | async def start_app(self, app_id: str, **_kwargs) -> Response: 283 | """ 284 | The start_app method starts an application. 285 | 286 | :param app_id: Specify the application by id 287 | :param _kwargs: Keyword arguments 288 | :return: A Response object 289 | :rtype: Response 290 | 291 | :raises NotFoundError: Raised when the request status code is 404 292 | :raises BadRequestError: Raised when the request status code is 400 293 | :raises AuthenticationFailure: Raised when the request status 294 | code is 401 295 | :raises TooManyRequestsError: Raised when the request status 296 | code is 429 297 | """ 298 | return await self._http.start_application(app_id) 299 | 300 | @validate 301 | @_notify_listener(Endpoint.stop()) 302 | async def stop_app(self, app_id: str, **_kwargs) -> Response: 303 | """ 304 | The stop_app method stops an application. 305 | 306 | :param app_id: Specify the application by id 307 | :param _kwargs: Keyword arguments 308 | :return: A Response object 309 | :rtype: Response 310 | 311 | :raises NotFoundError: Raised when the request status code is 404 312 | :raises BadRequestError: Raised when the request status code is 400 313 | :raises AuthenticationFailure: Raised when the request status 314 | code is 401 315 | :raises TooManyRequestsError: Raised when the request status 316 | code is 429 317 | """ 318 | return await self._http.stop_application(app_id) 319 | 320 | @validate 321 | @_notify_listener(Endpoint.restart()) 322 | async def restart_app(self, app_id: str, **_kwargs) -> Response: 323 | """ 324 | The restart_app method is restarts an application. 325 | 326 | :param app_id: Specify the application id 327 | :param _kwargs: Keyword arguments 328 | :return: A Response object 329 | :rtype: Response 330 | 331 | :raises NotFoundError: Raised when the request status code is 404 332 | :raises BadRequestError: Raised when the request status code is 400 333 | :raises AuthenticationFailure: Raised when the request status 334 | code is 401 335 | :raises TooManyRequestsError: Raised when the request status 336 | code is 429 337 | """ 338 | return await self._http.restart_application(app_id) 339 | 340 | @validate 341 | @_notify_listener(Endpoint.backup()) 342 | async def backup(self, app_id: str, **_kwargs) -> Backup: 343 | """ 344 | The backup method is used to backup an application. 345 | 346 | :param app_id: Specify the application id 347 | :param _kwargs: Keyword arguments 348 | :return: A Backup object 349 | :rtype: Backup 350 | 351 | :raises NotFoundError: Raised when the request status code is 404 352 | :raises BadRequestError: Raised when the request status code is 400 353 | :raises AuthenticationFailure: Raised when the request status 354 | code is 401 355 | :raises TooManyRequestsError: Raised when the request status 356 | code is 429 357 | """ 358 | response: Response = await self._http.backup(app_id) 359 | payload: dict[str, Any] = response.response 360 | return Backup(**payload) 361 | 362 | @validate 363 | @_notify_listener(Endpoint.delete_app()) 364 | async def delete_app(self, app_id: str, **_kwargs) -> Response: 365 | """ 366 | The delete_app method deletes an application. 367 | 368 | :param app_id: The application id 369 | :param _kwargs: Keyword arguments 370 | :return: A Response object 371 | :rtype: Response 372 | 373 | :raises NotFoundError: Raised when the request status code is 404 374 | :raises BadRequestError: Raised when the request status code is 400 375 | :raises AuthenticationFailure: Raised when the request status 376 | code is 401 377 | :raises TooManyRequestsError: Raised when the request status 378 | code is 429 379 | """ 380 | return await self._http.delete_application(app_id) 381 | 382 | @validate 383 | @_notify_listener(Endpoint.commit()) 384 | async def commit(self, app_id: str, file: File, **_kwargs) -> Response: 385 | """ 386 | The commit method is used to commit an application. 387 | 388 | :param app_id: Specify the application by id 389 | :param file: File: Specify the File object to be committed 390 | :param _kwargs: Keyword arguments 391 | :return: A Response object 392 | :rtype: Response 393 | 394 | :raises NotFoundError: Raised when the request status code is 404 395 | :raises BadRequestError: Raised when the request status code is 400 396 | :raises AuthenticationFailure: Raised when the request status 397 | code is 401 398 | :raises TooManyRequestsError: Raised when the request status 399 | code is 429 400 | """ 401 | return await self._http.commit(app_id, file) 402 | 403 | @validate 404 | @_notify_listener(Endpoint.user()) 405 | async def app(self, app_id: str, **_kwargs) -> Application: 406 | """ 407 | The app method returns an Application object. 408 | 409 | :param app_id: Specify the application by id 410 | :param _kwargs: Keyword arguments 411 | :return: An Application object 412 | :rtype: Application 413 | 414 | :raises ApplicationNotFound: Raised when is not found an application 415 | with the specified id 416 | :raises BadRequestError: Raised when the request status code is 400 417 | :raises AuthenticationFailure: Raised when the request status 418 | code is 401 419 | :raises TooManyRequestsError: Raised when the request status 420 | code is 429 421 | """ 422 | response: Response = await self._http.fetch_user_info() 423 | payload = response.response 424 | app_data = list( 425 | filter( 426 | lambda application: application["id"] == app_id, 427 | payload["applications"], 428 | ) 429 | ) 430 | if not app_data: 431 | raise ApplicationNotFound(app_id=app_id) 432 | app_data = app_data.pop() 433 | app_data = AppData(**app_data).to_dict() 434 | return Application(client=self, http=self._http, **app_data) 435 | 436 | # @_notify_listener(Endpoint.user()) 437 | async def all_apps(self, **_kwargs) -> list[int]: 438 | """ 439 | The all_apps method returns a list of all applications that the user 440 | has access to. 441 | 442 | :param _kwargs: Keyword arguments 443 | :return: A list of Application objects 444 | :rtype: list[Application] 445 | 446 | :raises BadRequestError: Raised when the request status code is 400 447 | :raises AuthenticationFailure: Raised when the request status 448 | code is 401 449 | :raises TooManyRequestsError: Raised when the request status 450 | code is 429 451 | """ 452 | response: Response = await self._http.fetch_user_info() 453 | payload = response.response 454 | apps_data: list = payload["applications"] 455 | apps: list[Application] = [] 456 | for data in apps_data: 457 | data = AppData(**data).to_dict() 458 | apps.append(Application(client=self, http=self._http, **data)) 459 | return apps 460 | 461 | @validate 462 | @_notify_listener(Endpoint.upload()) 463 | async def upload_app(self, file: File, **_kwargs) -> UploadData: 464 | """ 465 | The upload_app method uploads an application to the server. 466 | 467 | :param file: Upload a file 468 | :param _kwargs: Keyword arguments 469 | :return: An UploadData object 470 | :rtype: UploadData 471 | 472 | :raises NotFoundError: Raised when the request status code is 404 473 | :raises BadRequestError: Raised when the request status code is 400 474 | :raises AuthenticationFailure: Raised when the request status 475 | code is 401 476 | :raises TooManyRequestsError: Raised when the request status 477 | code is 429 478 | :raises FewMemory: Raised when user memory reached the maximum 479 | amount of memory 480 | :raises BadMemory: Raised when the memory in configuration file is 481 | invalid 482 | :raises MissingConfigFile: Raised when the .zip file is missing the 483 | config file (squarecloud.app/squarecloud.config) 484 | :raises MissingDependenciesFile: Raised when the .zip file is missing 485 | the dependencies file (requirements.txt, package.json, ...) 486 | :raises MissingMainFile: Raised when the .zip file is missing the main 487 | file (main.py, index.js, ...) 488 | :raises InvalidMain: Raised when the field MAIN in config file is 489 | invalid or when the main file is corrupted 490 | :raises InvalidDisplayName: Raised when the field DISPLAY_NAME 491 | in config file is invalid 492 | :raises MissingDisplayName: Raised when the DISPLAY_NAME field is 493 | missing in the config file 494 | :raises InvalidMemory: Raised when the MEMORY field is invalid 495 | :raises MissingMemory: Raised when the MEMORY field is missing in 496 | the config file 497 | :raises InvalidVersion: Raised when the VERSION field is invalid, 498 | the value accepted is "recommended" or "latest" 499 | :raises MissingVersion: Raised when the VERSION field is missing in 500 | the config file 501 | :raises InvalidAccessToken: Raised when a GitHub access token 502 | provided is invalid 503 | :raises InvalidDomain: Raised when a domain provided is invalid 504 | """ 505 | if not isinstance(file, File): 506 | raise InvalidFile(f"you need provide an {File.__name__} object") 507 | 508 | if (file.filename is not None) and ( 509 | file.filename.split(".")[-1] != "zip" 510 | ): 511 | raise InvalidFile("the file must be a .zip file") 512 | response: Response = await self._http.upload(file) 513 | payload: dict[str, Any] = response.response 514 | return UploadData(**payload) 515 | 516 | @validate 517 | @_notify_listener(Endpoint.files_list()) 518 | async def app_files_list( 519 | self, app_id: str, path: str, **_kwargs 520 | ) -> list[FileInfo]: 521 | """ 522 | The app_files_list method returns a list of your application files. 523 | 524 | :param app_id: Specify the application by id 525 | :param path: Specify the path to the file 526 | :param _kwargs: Keyword arguments 527 | :return: A list of FileInfo objects 528 | :rtype: list[FileInfo] 529 | 530 | :raises NotFoundError: Raised when the request status code is 404 531 | :raises BadRequestError: Raised when the request status code is 400 532 | :raises AuthenticationFailure: Raised when the request status 533 | code is 401 534 | :raises TooManyRequestsError: Raised when the request status 535 | code is 429 536 | """ 537 | response: Response = await self._http.fetch_app_files_list( 538 | app_id, path 539 | ) 540 | if not response.response: 541 | return [] 542 | return [ 543 | FileInfo(**data, app_id=app_id, path=path + f"/{data.get('name')}") 544 | for data in response.response 545 | ] 546 | 547 | @validate 548 | @_notify_listener(Endpoint.files_read()) 549 | async def read_app_file( 550 | self, app_id: str, path: str, **_kwargs 551 | ) -> BytesIO | None: 552 | """ 553 | The read_app_file method reads a file from the specified path and 554 | returns a BytesIO representation. 555 | 556 | :param app_id: Specify the application by id 557 | :param path: str: Specify the path of the file to be read 558 | :param _kwargs: Keyword arguments 559 | :return: A BytesIO representation of the file 560 | :rtype: BytesIO | None 561 | 562 | :raises NotFoundError: Raised when the request status code is 404 563 | :raises BadRequestError: Raised when the request status code is 400 564 | :raises AuthenticationFailure: Raised when the request status 565 | code is 401 566 | :raises TooManyRequestsError: Raised when the request status 567 | code is 429 568 | """ 569 | response: Response = await self._http.read_app_file(app_id, path) 570 | if response.response: 571 | return BytesIO(bytes(response.response.get("data"))) 572 | return None 573 | 574 | @validate 575 | @_notify_listener(Endpoint.files_create()) 576 | async def create_app_file( 577 | self, app_id: str, file: File, path: str, **_kwargs 578 | ) -> Response: 579 | """ 580 | The create_app_file method creates a new file in the specified 581 | directory. 582 | 583 | :param app_id: Specify the application by id 584 | :param file: Pass the file to be created 585 | :param path: Specify the directory to create the file in 586 | :param _kwargs: Keyword arguments 587 | :return: A Response object 588 | :rtype: Response 589 | 590 | :raises NotFoundError: Raised when the request status code is 404 591 | :raises BadRequestError: Raised when the request status code is 400 592 | :raises AuthenticationFailure: Raised when the request status 593 | code is 401 594 | :raises TooManyRequestsError: Raised when the request status 595 | code is 429 596 | """ 597 | if not isinstance(file, File): 598 | raise SquareException( 599 | "the file must be an string or a squarecloud.File object" 600 | ) 601 | file_bytes = list(file.bytes.read()) 602 | response: Response = await self._http.create_app_file( 603 | app_id, file_bytes, path=path 604 | ) 605 | file.bytes.close() 606 | 607 | return response 608 | 609 | @validate 610 | @_notify_listener(Endpoint.files_delete()) 611 | async def delete_app_file( 612 | self, app_id: str, path: str, **_kwargs 613 | ) -> Response: 614 | """ 615 | The delete_app_file method deletes a file in the specified directory. 616 | 617 | :param app_id: Specify the application byd id 618 | :param path: Specify the directory where the file should be 619 | deleted 620 | :param _kwargs: Keyword arguments 621 | :return: A Response object 622 | :rtype: Response 623 | 624 | :raises NotFoundError: Raised when the request status code is 404 625 | :raises BadRequestError: Raised when the request status code is 400 626 | :raises AuthenticationFailure: Raised when the request status 627 | code is 401 628 | :raises TooManyRequestsError: Raised when the request status 629 | code is 429 630 | """ 631 | return await self._http.file_delete(app_id, path) 632 | 633 | @validate 634 | @_notify_listener(Endpoint.last_deploys()) 635 | async def last_deploys( 636 | self, app_id: str, **_kwargs 637 | ) -> list[list[DeployData]]: 638 | """ 639 | The last_deploys method returns a list of DeployData objects. 640 | 641 | :param self: Represent the instance of a class 642 | :param app_id: str: Specify the application by id 643 | :param _kwargs: Keyword arguments 644 | :return: A list of DeployData objects 645 | :rtype: list[list[DeployData]] 646 | 647 | :raises NotFoundError: Raised when the request status code is 404 648 | :raises BadRequestError: Raised when the request status code is 400 649 | :raises AuthenticationFailure: Raised when the request status 650 | code is 401 651 | :raises TooManyRequestsError: Raised when the request status 652 | code is 429 653 | """ 654 | response: Response = await self._http.get_last_deploys(app_id) 655 | data = response.response 656 | return [[DeployData(**deploy) for deploy in _] for _ in data] 657 | 658 | @validate 659 | @_notify_listener(Endpoint.github_integration()) 660 | async def github_integration( 661 | self, app_id: str, access_token: str, **_kwargs 662 | ) -> str: 663 | """ 664 | The github_integration method returns a GitHub Webhook url to integrate 665 | with your GitHub repository 666 | 667 | :param app_id: Specify the application by id 668 | :param access_token: your GitHub access token 669 | :param _kwargs: Keyword arguments 670 | :return: A GitHub Webhook url 671 | 672 | :raises InvalidAccessToken: Raised when a GitHub access token 673 | provided is invalid 674 | :raises NotFoundError: Raised when the request status code is 404 675 | :raises BadRequestError: Raised when the request status code is 400 676 | :raises AuthenticationFailure: Raised when the request status 677 | code is 401 678 | :raises TooManyRequestsError: Raised when the request status 679 | code is 429 680 | """ 681 | response: Response = await self._http.create_github_integration( 682 | app_id=app_id, github_access_token=access_token 683 | ) 684 | data = response.response 685 | return data.get("webhook") 686 | 687 | @validate 688 | @_notify_listener(Endpoint.custom_domain()) 689 | async def set_custom_domain( 690 | self, app_id: str, custom_domain: str, **_kwargs 691 | ) -> Response: 692 | """ 693 | The set_custom_domain method sets a custom domain to your website 694 | 695 | :param app_id: Specify the application by id 696 | :param custom_domain: Specify the custom domain to use for your website 697 | :param _kwargs: Keyword arguments 698 | :return: A Response object 699 | :rtype: Response 700 | 701 | :raises InvalidDomain: Raised when a domain provided is invalid 702 | :raises NotFoundError: Raised when the request status code is 404 703 | :raises BadRequestError: Raised when the request status code is 400 704 | :raises AuthenticationFailure: Raised when the request status 705 | code is 401 706 | :raises TooManyRequestsError: Raised when the request status 707 | code is 429 708 | """ 709 | return await self._http.update_custom_domain( 710 | app_id=app_id, custom_domain=custom_domain 711 | ) 712 | 713 | @validate 714 | @_notify_listener(Endpoint.domain_analytics()) 715 | async def domain_analytics( 716 | self, app_id: str, **_kwargs 717 | ) -> DomainAnalytics: 718 | """ 719 | The domain_analytics method return a DomainAnalytics object 720 | 721 | :param app_id: Specify the application by id 722 | :param _kwargs: Keyword arguments 723 | :return: A DomainAnalytics object 724 | :rtype: DomainAnalytics 725 | 726 | :raises NotFoundError: Raised when the request status code is 404 727 | :raises BadRequestError: Raised when the request status code is 400 728 | :raises AuthenticationFailure: Raised when the request status 729 | code is 401 730 | :raises TooManyRequestsError: Raised when the request status 731 | code is 429 732 | """ 733 | response: Response = await self._http.domain_analytics( 734 | app_id=app_id, 735 | ) 736 | 737 | return DomainAnalytics(**response.response) 738 | 739 | @validate 740 | @_notify_listener(Endpoint.all_backups()) 741 | async def all_app_backups( 742 | self, app_id: str, **_kwargs 743 | ) -> list[BackupInfo]: 744 | response: Response = await self._http.get_all_app_backups( 745 | app_id=app_id 746 | ) 747 | return [BackupInfo(**backup_data) for backup_data in response.response] 748 | 749 | @_notify_listener(Endpoint.all_apps_status()) 750 | async def all_apps_status(self, **_kwargs) -> list[ResumedStatus]: 751 | response: Response = await self._http.all_apps_status() 752 | all_status = [] 753 | for status in response.response: 754 | if status["running"] is True: 755 | all_status.append(ResumedStatus(**status)) 756 | return all_status 757 | 758 | @validate 759 | @_notify_listener(Endpoint.move_file()) 760 | async def move_app_file( 761 | self, app_id: str, origin: str, dest: str, **_kwargs 762 | ) -> Response: 763 | response: Response = await self._http.move_app_file( 764 | app_id=app_id, origin=origin, dest=dest 765 | ) 766 | return response 767 | 768 | @validate 769 | @_notify_listener(Endpoint.dns_records()) 770 | async def dns_records(self, app_id: str) -> list[DNSRecord]: 771 | response: Response = await self._http.dns_records(app_id) 772 | return [DNSRecord(**data) for data in response.response] 773 | 774 | @validate 775 | @_notify_listener(Endpoint.current_integration()) 776 | async def current_app_integration(self, app_id: str) -> str | None: 777 | response: Response = await self._http.get_app_current_integration( 778 | app_id 779 | ) 780 | return response.response["webhook"] 781 | -------------------------------------------------------------------------------- /squarecloud/data.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | import zipfile 5 | from datetime import datetime 6 | from typing import Any, Literal 7 | 8 | from ._internal.constants import USING_PYDANTIC 9 | from .http import HTTPClient 10 | 11 | if USING_PYDANTIC: 12 | from pydantic.dataclasses import dataclass 13 | else: 14 | from dataclasses import dataclass 15 | 16 | 17 | class DataClasMeta(type): 18 | def __new__(cls, name: str, bases: tuple, dct: dict[str, Any]) -> type: 19 | new_class = super().__new__(cls, name, bases, dct) 20 | return dataclass(frozen=True)(new_class) 21 | 22 | 23 | class BaseDataClass(metaclass=DataClasMeta): 24 | def to_dict(self) -> dict[str, str | dict[str, Any]]: 25 | return self.__dict__.copy() 26 | 27 | 28 | class PlanData(BaseDataClass): 29 | """ 30 | Plan data class 31 | 32 | :ivar name: The plan name 33 | :ivar memory: The plan memory available 34 | :ivar duration: Plan duration 35 | 36 | :type name: str 37 | :type memory: Dict[str, Any] 38 | :type duration: Dict[str, Any] 39 | """ 40 | 41 | name: str 42 | memory: dict[str, Any] 43 | duration: dict[str, Any] | None 44 | 45 | 46 | class Language(BaseDataClass): 47 | name: str 48 | version: str 49 | 50 | 51 | class StatusData(BaseDataClass): 52 | """ 53 | Application status class 54 | 55 | :ivar cpu: the cpu used 56 | :ivar ram: the ram used 57 | :ivar status: the actual status of the application 58 | :ivar running: weather the application is running 59 | :ivar storage: storage used by the application 60 | :ivar network: network information 61 | :ivar uptime: uptime of the application 62 | :ivar time: time of the application 63 | 64 | :type cpu: str 65 | :type ram: str 66 | :type status: str 67 | :type running: bool 68 | :type storage: str 69 | :type network: Dict[str, Any] 70 | :type requests: conint(ge=0) 71 | :type uptime: int 72 | :type time: int | None = None 73 | """ 74 | 75 | cpu: str 76 | ram: str 77 | status: str 78 | running: bool 79 | storage: str 80 | network: dict[str, Any] 81 | uptime: int | None = None 82 | time: int | None = None 83 | 84 | 85 | class ResumedStatus(BaseDataClass): 86 | id: str 87 | running: bool 88 | cpu: str 89 | ram: str 90 | 91 | 92 | class AppData(BaseDataClass): 93 | """ 94 | Application data class 95 | 96 | :ivar id: The application ID 97 | :ivar name: The application name 98 | :ivar cluster: The cluster that the app is hosted on 99 | :ivar ram: The amount of RAM that application is using 100 | :ivar language The programming language of the app.: 101 | 102 | :type id: str 103 | :type name: str 104 | :type cluster: str 105 | :type ram: confloat(ge=0); 106 | :type lang: Language 107 | :type domain: str | None = None 108 | :type custom: str | None = None 109 | :type desc: str | None = None 110 | """ 111 | 112 | id: str 113 | name: str 114 | cluster: str 115 | ram: float 116 | lang: str | None 117 | cluster: str 118 | domain: str | None = None 119 | custom: str | None = None 120 | desc: str | None = None 121 | 122 | 123 | class UserData(BaseDataClass): 124 | """ 125 | User data class 126 | 127 | :ivar id: User ID; 128 | :ivar name: Username 129 | :ivar plan: User plan 130 | :ivar email: User email 131 | 132 | :type id: int 133 | :type name: str 134 | :type plan: PlanData 135 | :type email: str | None = None 136 | """ 137 | 138 | id: int 139 | name: str 140 | plan: PlanData 141 | email: str | None = None 142 | 143 | 144 | class LogsData(BaseDataClass): 145 | """Logs data class 146 | 147 | :ivar logs: A string containing logs of your application 148 | 149 | :type logs: str | str = '' 150 | """ 151 | 152 | logs: str = '' 153 | 154 | def __eq__(self, other: object) -> bool: 155 | """ 156 | The __eq__ function is a special function that allows us to compare 157 | two objects of the same class. 158 | In this case, we are comparing two LogsData objects. The __eq__ 159 | function returns True if the logs 160 | of both LogsData objects are equal and False otherwise. 161 | 162 | Example: 163 | 164 | ````{.py3 hl_lines="15 21" linenums="1" title="example_2.py"} 165 | from time import sleep 166 | 167 | import squarecloud as square 168 | 169 | client = square.Client(api_key='API KEY') 170 | 171 | 172 | async def example(): 173 | app = await client.app('application id') 174 | 175 | logs1 = await app.logs() # 'Hello World' 176 | 177 | logs2 = await app.logs() # 'Hello World' 178 | 179 | print(logs1 == logs2) # True 180 | 181 | sleep(10) 182 | 183 | logs3 = await app.logs() # 'Hello World, I'm beautifully' 184 | 185 | print(logs1 == logs3) # False 186 | ```` 187 | 188 | :param self: Refer to the object itself 189 | :param other: Compare the current instance of LogsData to another 190 | instance of LogsData 191 | :return: A boolean value that is true if the two objects are equal and 192 | false otherwise 193 | :rtype: bool 194 | """ 195 | return isinstance(other, LogsData) and self.logs == other.logs 196 | 197 | 198 | class BackupInfo(BaseDataClass): 199 | name: str 200 | size: int 201 | modified: datetime 202 | key: str 203 | 204 | 205 | class Backup: 206 | """ 207 | Backup data class 208 | 209 | :ivar url: Url for download your backup 210 | :ivar key: The backup's key 211 | 212 | :type url: str 213 | :type key: str 214 | """ 215 | 216 | __slots__ = ('url', 'key') 217 | 218 | def __init__(self, url: str, key: str) -> None: 219 | self.url = url 220 | self.key = key 221 | 222 | def to_dict(self) -> None: 223 | return {'url': self.url, 'key': self.key} 224 | 225 | async def download(self, path: str = './') -> zipfile.ZipFile: 226 | file_name = os.path.basename(self.url.split('?')[0]) 227 | content = await HTTPClient.fetch_backup_content(self.url) 228 | with zipfile.ZipFile(f'{path}/{file_name}', 'w') as zip_file: 229 | zip_file.writestr(f'{path}/{file_name}', content) 230 | return zip_file 231 | 232 | 233 | class UploadData(BaseDataClass): 234 | """ 235 | Upload data class 236 | 237 | :ivar id: ID of the uploaded application 238 | :ivar name: Tag of the uploaded application 239 | :ivar language: Programming language of the uploaded application 240 | :ivar ram: Ram allocated for the uploaded application 241 | :ivar cpu: Cpu of the uploaded application 242 | :ivar description: Description of the uploaded application 243 | :ivar subdomain: Subdomain of the uploaded application (only in websites) 244 | 245 | :type id: str 246 | :type name: str 247 | :type language: Language 248 | :type ram: confloat(ge=0) 249 | :type cpu: confloat(ge=0) 250 | :type subdomain: str | None = None 251 | :type description: str | None = None 252 | """ 253 | 254 | id: str 255 | name: str 256 | language: Language 257 | ram: float 258 | cpu: float 259 | domain: str | None = None 260 | description: str | None = None 261 | 262 | 263 | class FileInfo(BaseDataClass): 264 | """ 265 | File information 266 | 267 | :ivar type: return type of file 268 | :ivar name: File/Directory name 269 | :ivar size: File size 270 | :ivar lastModified: Last modification time 271 | :ivar path: File/Directory path 272 | 273 | :type type: Literal['file', 'directory'] 274 | :type name: str 275 | :type size: int 276 | :type lastModified: int | float | None 277 | :type path: str 278 | """ 279 | 280 | app_id: str 281 | type: Literal['file', 'directory'] 282 | name: str 283 | lastModified: int | float | None # noqa: N815: Ignore mixedCase naming convention 284 | path: str 285 | size: int = 0 286 | 287 | 288 | class DeployData(BaseDataClass): 289 | id: str 290 | state: str 291 | date: datetime 292 | 293 | 294 | class AnalyticsTotal(BaseDataClass): 295 | visits: int 296 | megabytes: float 297 | bytes: int 298 | 299 | 300 | class DomainAnalytics(BaseDataClass): 301 | class Analytics(BaseDataClass): 302 | total: list[AnalyticsTotal] 303 | countries: list[Any] 304 | methods: list[Any] 305 | referers: list[Any] 306 | browsers: list[Any] 307 | deviceTypes: list[ 308 | Any 309 | ] # noqa: N815: Ignore mixedCase naming convention 310 | operatingSystems: list[ 311 | Any 312 | ] # noqa: N815: Ignore mixedCase naming convention 313 | agents: list[Any] 314 | hosts: list[Any] 315 | paths: list[Any] 316 | 317 | class Domain(BaseDataClass): 318 | hostname: str 319 | analytics: DomainAnalytics.Analytics | None 320 | 321 | class Custom(BaseDataClass): 322 | analytics: DomainAnalytics.Analytics | None 323 | 324 | domain: Domain 325 | custom: Custom 326 | 327 | 328 | class DNSRecord(BaseDataClass): 329 | type: str 330 | name: str 331 | value: str 332 | status: str 333 | -------------------------------------------------------------------------------- /squarecloud/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | 4 | class SquareException(Exception): 5 | """abstract class SquareException""" 6 | 7 | def __init__(self, message: str = 'An unexpected error occurred') -> None: 8 | self.message = message 9 | 10 | def __str__(self) -> str: 11 | return self.message 12 | 13 | 14 | class RequestError(SquareException): 15 | """raised when a request fails""" 16 | 17 | def __init__(self, route: str, status_code: int, code: str) -> None: 18 | self.route = route 19 | self.status = status_code 20 | self.code = code 21 | self.message = f'route [{route}] returned {status_code}, [{code}]' 22 | super().__init__(self.message) 23 | 24 | 25 | class AuthenticationFailure(RequestError): 26 | """raised when an API token is invalid""" 27 | 28 | def __init__(self, *args, **kwargs) -> None: 29 | super().__init__(*args, **kwargs) 30 | self.message = ( 31 | 'Authentication failed: ' 'Invalid API token or access denied' 32 | ) 33 | 34 | 35 | class NotFoundError(RequestError): 36 | """raises when a request returns a 404 response""" 37 | 38 | def __init__(self, *args, **kwargs) -> None: 39 | super().__init__(*args, **kwargs) 40 | self.message = 'Resource not found: 404' 41 | 42 | 43 | class BadRequestError(RequestError): 44 | """raises when a request returns a 400 response""" 45 | 46 | def __init__(self, *args, **kwargs) -> None: 47 | super().__init__(*args, **kwargs) 48 | 49 | 50 | class ApplicationNotFound(SquareException): 51 | """raises when an application is not found""" 52 | 53 | def __init__(self, app_id: str, *args, **kwargs) -> None: 54 | super().__init__(*args, **kwargs) 55 | self.app_id = app_id 56 | self.message = f'No application was found with id: {app_id}' 57 | 58 | 59 | class InvalidFile(SquareException): 60 | """raised when a file is invalid""" 61 | 62 | def __init__(self, *args, **kwargs) -> None: 63 | super().__init__(*args, **kwargs) 64 | self.message = 'Invalid file' 65 | 66 | 67 | class MissingConfigFile(RequestError): 68 | """raised when the configuration file is missing""" 69 | 70 | def __init__(self, *args, **kwargs) -> None: 71 | super().__init__(*args, **kwargs) 72 | self.message = 'Configuration file is missing' 73 | 74 | 75 | class MissingDependenciesFile(RequestError): 76 | """raised when the dependencies file is missing""" 77 | 78 | def __init__(self, *args, **kwargs) -> None: 79 | super().__init__(*args, **kwargs) 80 | self.message = 'Dependencies file is missing' 81 | 82 | 83 | class TooManyRequests(RequestError): 84 | """raised when there are too many requests""" 85 | 86 | def __init__(self, *args, **kwargs) -> None: 87 | super().__init__(*args, **kwargs) 88 | self.message = 'Too many requests' 89 | 90 | 91 | class FewMemory(RequestError): 92 | """ 93 | raised when there is insufficient memory available to host an application. 94 | """ 95 | 96 | def __init__(self, *args, **kwargs) -> None: 97 | super().__init__(*args, **kwargs) 98 | self.message = 'Insufficient memory available' 99 | 100 | 101 | class BadMemory(RequestError): 102 | """ 103 | raised when the user has no memory to host an application. 104 | """ 105 | 106 | def __init__(self, *args, **kwargs) -> None: 107 | super().__init__(*args, **kwargs) 108 | self.message = 'No memory available' 109 | 110 | 111 | class InvalidConfig(RequestError): 112 | """ 113 | raised when the config file is corrupt or invalid. 114 | """ 115 | 116 | def __init__(self, *args, **kwargs) -> None: 117 | super().__init__(*args, **kwargs) 118 | self.message = 'invalid config file' 119 | 120 | 121 | class InvalidDisplayName(InvalidConfig): 122 | """ 123 | raised when the display name in the config file is invalid. 124 | """ 125 | 126 | def __init__(self, *args, **kwargs) -> None: 127 | super().__init__(*args, **kwargs) 128 | self.message = 'Invalid display name in config file' 129 | 130 | 131 | class MissingDisplayName(InvalidConfig): 132 | """ 133 | raised when the display name in the config file is missing. 134 | """ 135 | 136 | def __init__(self, *args, **kwargs) -> None: 137 | super().__init__(*args, **kwargs) 138 | self.message = 'Display name is missing in the config file' 139 | 140 | 141 | class InvalidMain(InvalidConfig): 142 | """ 143 | raised when the main file in the config file is invalid. 144 | """ 145 | 146 | def __init__(self, *args, **kwargs) -> None: 147 | super().__init__(*args, **kwargs) 148 | self.message = 'Invalid main file in config file' 149 | 150 | 151 | class MissingMainFile(InvalidConfig): 152 | """ 153 | raised when the main file in the config file is missing. 154 | """ 155 | 156 | def __init__(self, *args, **kwargs) -> None: 157 | super().__init__(*args, **kwargs) 158 | self.message = 'Main file is missing in the config file' 159 | 160 | 161 | class InvalidMemory(InvalidConfig): 162 | """ 163 | raised when the memory value in the config file is invalid. 164 | """ 165 | 166 | def __init__(self, *args, **kwargs) -> None: 167 | super().__init__(*args, **kwargs) 168 | self.message = 'Invalid memory value in config file' 169 | 170 | 171 | class MissingMemory(InvalidConfig): 172 | """ 173 | raised when the memory value in the config file is missing. 174 | """ 175 | 176 | def __init__(self, *args, **kwargs) -> None: 177 | super().__init__(*args, **kwargs) 178 | self.message = 'Memory value is missing in the config file' 179 | 180 | 181 | class InvalidVersion(InvalidConfig): 182 | """ 183 | raised when the version value in the config file is invalid. 184 | """ 185 | 186 | def __init__(self, *args, **kwargs) -> None: 187 | super().__init__(*args, **kwargs) 188 | self.message = 'Invalid version value in config file' 189 | 190 | 191 | class MissingVersion(InvalidConfig): 192 | """ 193 | raised when the version value in the config file is missing. 194 | """ 195 | 196 | def __init__(self, *args, **kwargs) -> None: 197 | super().__init__(*args, **kwargs) 198 | self.message = 'Version value is missing in the config file' 199 | 200 | 201 | class InvalidAccessToken(RequestError): 202 | """ 203 | raised when the GitHub access token provided by the user is invalid. 204 | """ 205 | 206 | def __init__(self, *args, **kwargs) -> None: 207 | super().__init__(*args, **kwargs) 208 | 209 | 210 | class InvalidDomain(RequestError): 211 | """ 212 | raised when an invalid domain is provided 213 | """ 214 | 215 | def __init__(self, domain: str, *args, **kwargs) -> None: 216 | super().__init__(*args, **kwargs) 217 | self.message = f'"{domain}" is a invalid custom domain' 218 | 219 | 220 | class InvalidStart(InvalidConfig): 221 | def __init__(self, *args, **kwargs) -> None: 222 | super().__init__(*args, **kwargs) 223 | self.message = 'Invalid start value in configuration file' 224 | 225 | 226 | class InvalidListener(SquareException): 227 | def __init__(self, listener: Callable, *args, **kwargs) -> None: 228 | super().__init__(*args, **kwargs) 229 | self.listener = listener 230 | -------------------------------------------------------------------------------- /squarecloud/file.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import io 4 | import os 5 | from typing import Any 6 | 7 | from squarecloud import errors 8 | 9 | 10 | class File: 11 | """ 12 | File object 13 | 14 | You can use a file already opened or pass the file path. 15 | NOTE: To pass binary data, consider usage of `io.BytesIO`. 16 | """ 17 | 18 | __slots__ = ('bytes', 'filename') 19 | 20 | def __init__( 21 | self, 22 | fp: str | bytes | os.PathLike[Any] | io.BufferedIOBase, 23 | filename: str | None = None, 24 | ) -> None: 25 | """ 26 | The __init__ function is called when the class is instantiated. 27 | It sets up the instance of the class, and it's where you put all your 28 | initialization code. 29 | The __init__ function takes at least one argument: self, which refers 30 | to the object being created. 31 | 32 | :param self: Refer to the class instance 33 | :param fp: str | bytes | os.PathLike[Any] | io.BufferedIOBase: Define 34 | the file path, 35 | :param filename: str | None: Set the filename attribute of the class 36 | :return: None 37 | """ 38 | if isinstance(fp, io.BufferedIOBase): 39 | if not (fp.seekable() and fp.readable()): 40 | raise ValueError( 41 | f'File buffer {fp!r} must be seekable and readable' 42 | ) 43 | self.bytes: io.BufferedIOBase = fp 44 | else: 45 | # Verificar se fp é bytes (dados binários) e criar um io.BytesIO 46 | if isinstance(fp, bytes): 47 | self.bytes = io.BytesIO(fp) 48 | else: 49 | self.bytes = open(fp, 'rb') 50 | 51 | if filename is None: 52 | if isinstance(fp, str): 53 | _, filename = os.path.split(fp) 54 | else: 55 | filename = getattr(fp, 'name', None) 56 | 57 | if not filename: 58 | raise errors.SquareException('You need provide a filename') 59 | 60 | self.filename: str | None = filename 61 | -------------------------------------------------------------------------------- /squarecloud/http/__init__.py: -------------------------------------------------------------------------------- 1 | from .endpoints import Endpoint 2 | from .http_client import HTTPClient, Response 3 | 4 | __all__ = ['HTTPClient', 'Response', 'Endpoint'] 5 | -------------------------------------------------------------------------------- /squarecloud/http/endpoints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Endpoint: 5 | """Class representing an API endpoint.""" 6 | 7 | ENDPOINTS_V2 = { 8 | 'USER': {'METHOD': 'GET', 'PATH': '/users/me'}, 9 | 'APP_DATA': {'METHOD': 'GET', 'PATH': '/apps/{app_id}'}, 10 | 'APP_STATUS': {'METHOD': 'GET', 'PATH': '/apps/{app_id}/status'}, 11 | 'ALL_APPS_STATUS': {'METHOD': 'GET', 'PATH': '/apps/status'}, 12 | 'ALL_BACKUPS': {'METHOD': 'GET', 'PATH': '/apps/{app_id}/backups'}, 13 | 'LOGS': {'METHOD': 'GET', 'PATH': '/apps/{app_id}/logs'}, 14 | 'START': {'METHOD': 'POST', 'PATH': '/apps/{app_id}/start'}, 15 | 'STOP': {'METHOD': 'POST', 'PATH': '/apps/{app_id}/stop'}, 16 | 'RESTART': {'METHOD': 'POST', 'PATH': '/apps/{app_id}/restart'}, 17 | 'BACKUP': {'METHOD': 'POST', 'PATH': '/apps/{app_id}/backups'}, 18 | 'COMMIT': {'METHOD': 'POST', 'PATH': '/apps/{app_id}/commit'}, 19 | 'DELETE_APP': {'METHOD': 'DELETE', 'PATH': '/apps/{app_id}'}, 20 | 'UPLOAD_APP': {'METHOD': 'POST', 'PATH': '/apps'}, 21 | 'FILES_LIST': { 22 | 'METHOD': 'GET', 23 | 'PATH': '/apps/{app_id}/files?path={path}', 24 | }, 25 | 'FILES_READ': { 26 | 'METHOD': 'GET', 27 | 'PATH': '/apps/{app_id}/files/content?path={path}', 28 | }, 29 | 'FILES_CREATE': { 30 | 'METHOD': 'PUT', 31 | 'PATH': '/apps/{app_id}/files', 32 | }, 33 | 'FILES_DELETE': { 34 | 'METHOD': 'DELETE', 35 | 'PATH': '/apps/{app_id}/files', 36 | }, 37 | 'MOVE_FILE': {'METHOD': 'PATCH', 'PATH': '/apps/{app_id}/files'}, 38 | 'LAST_DEPLOYS': { 39 | 'METHOD': 'GET', 40 | 'PATH': '/apps/{app_id}/deployments', 41 | }, 42 | 'CURRENT_INTEGRATION': { 43 | 'METHOD': 'GET', 44 | 'PATH': '/apps/{app_id}/deployments/current', 45 | }, 46 | 'GITHUB_INTEGRATION': { 47 | 'METHOD': 'POST', 48 | 'PATH': '/apps/{app_id}/deploy/webhook', 49 | }, 50 | 'CUSTOM_DOMAIN': { 51 | 'METHOD': 'POST', 52 | 'PATH': '/apps/{app_id}/network/custom', 53 | }, 54 | 'DOMAIN_ANALYTICS': { 55 | 'METHOD': 'GET', 56 | 'PATH': '/apps/{app_id}/network/analytics', 57 | }, 58 | 'DNSRECORDS': {'METHOD': 'GET', 'PATH': '/apps/{app_id}/network/dns'}, 59 | } 60 | 61 | def __init__(self, name: str) -> None: 62 | """ 63 | Initialize an Endpoint instance with the given name. 64 | 65 | :param name: The name of the endpoint. 66 | :raises ValueError: If the endpoint name is invalid. 67 | """ 68 | if not (endpoint := self.ENDPOINTS_V2.get(name)): 69 | raise ValueError(f"Invalid endpoint: '{name}'") 70 | self.name: str = name 71 | self.method: str = endpoint['METHOD'] 72 | self.path: str = endpoint['PATH'] 73 | 74 | def __eq__(self, other: object) -> bool: 75 | """ 76 | Compare two Endpoint instances for equality. 77 | 78 | :param other: The other Endpoint instance to compare. 79 | :return: True if both instances have the same name, otherwise False. 80 | """ 81 | return isinstance(other, Endpoint) and self.name == other.name 82 | 83 | def __repr__(self) -> str: 84 | """ 85 | Return the official string representation of the Endpoint instance. 86 | 87 | :return: A string representation of the Endpoint instance. 88 | """ 89 | return f"{Endpoint.__name__}('{self.name}')" 90 | 91 | @classmethod 92 | def user(cls) -> Endpoint: 93 | """ 94 | Returns an Endpoint object that represents the 95 | /user endpoint. 96 | """ 97 | return cls('USER') 98 | 99 | @classmethod 100 | def app_data(cls) -> Endpoint: 101 | """ 102 | Returns an Endpoint object that represents the 103 | /apps/{app_id} endpoint. 104 | """ 105 | return cls('APP_DATA') 106 | 107 | @classmethod 108 | def app_status(cls) -> Endpoint: 109 | """ 110 | Returns an Endpoint object that represents the 111 | /apps/{app_id}/status endpoint. 112 | """ 113 | return cls('APP_STATUS') 114 | 115 | @classmethod 116 | def logs(cls) -> Endpoint: 117 | """ 118 | Returns an Endpoint object that represents the 119 | /apps/{app_id}/logs endpoint. 120 | """ 121 | return cls('LOGS') 122 | 123 | @classmethod 124 | def start(cls) -> Endpoint: 125 | """ 126 | Returns an Endpoint object that represents the 127 | /apps/{app_id}/start endpoint. 128 | """ 129 | return cls('START') 130 | 131 | @classmethod 132 | def stop(cls) -> Endpoint: 133 | """ 134 | Returns an Endpoint object that represents the 135 | /apps/{app_id}/stop endpoint. 136 | """ 137 | return cls('STOP') 138 | 139 | @classmethod 140 | def restart(cls) -> Endpoint: 141 | """ 142 | Returns an Endpoint object that represents the 143 | /apps/{app_id}/restart endpoint. 144 | """ 145 | return cls('RESTART') 146 | 147 | @classmethod 148 | def backup(cls) -> Endpoint: 149 | """ 150 | Returns an Endpoint object that represents the 151 | /apps/{app_id}/backups endpoint. 152 | """ 153 | return cls('BACKUP') 154 | 155 | @classmethod 156 | def commit(cls) -> Endpoint: 157 | """ 158 | Returns an Endpoint object that represents the 159 | /apps/{app_id}/commit endpoint. 160 | """ 161 | return cls('COMMIT') 162 | 163 | @classmethod 164 | def delete_app(cls) -> Endpoint: 165 | """ 166 | Returns an Endpoint object that represents the 167 | /apps/{app_id} endpoint. 168 | """ 169 | return cls('DELETE_APP') 170 | 171 | @classmethod 172 | def upload(cls) -> Endpoint: 173 | """ 174 | Returns an Endpoint object that represents the 175 | /apps endpoint. 176 | """ 177 | return cls('UPLOAD_APP') 178 | 179 | @classmethod 180 | def files_list(cls) -> Endpoint: 181 | """ 182 | Returns an Endpoint object that represents the 183 | /apps/{app_id}/files/list endpoint. 184 | """ 185 | return cls('FILES_LIST') 186 | 187 | @classmethod 188 | def files_read(cls) -> Endpoint: 189 | """ 190 | Returns an Endpoint object that represents the 191 | /apps/{app_id}/files/read endpoint. 192 | """ 193 | return cls('FILES_READ') 194 | 195 | @classmethod 196 | def files_create(cls) -> Endpoint: 197 | """ 198 | Returns an Endpoint object that represents the 199 | /apps/{app_id}/files/create endpoint. 200 | """ 201 | return cls('FILES_CREATE') 202 | 203 | @classmethod 204 | def files_delete(cls) -> Endpoint: 205 | """ 206 | Returns an Endpoint object that represents the 207 | /apps/{app_id}/files/delete endpoint. 208 | """ 209 | return cls('FILES_DELETE') 210 | 211 | @classmethod 212 | def last_deploys(cls) -> Endpoint: 213 | """ 214 | Returns an Endpoint object that represents the 215 | /apps/{app_id}/deployments endpoint. 216 | """ 217 | return cls('LAST_DEPLOYS') 218 | 219 | @classmethod 220 | def github_integration(cls) -> Endpoint: 221 | """ 222 | Returns an Endpoint object that represents the 223 | /apps/{app_id}/deploy/webhook endpoint. 224 | """ 225 | return cls('GITHUB_INTEGRATION') 226 | 227 | @classmethod 228 | def domain_analytics(cls) -> Endpoint: 229 | """ 230 | Returns an Endpoint object that represents the 231 | /apps/{app_id}/network/analytics endpoint. 232 | """ 233 | return cls('DOMAIN_ANALYTICS') 234 | 235 | @classmethod 236 | def custom_domain(cls) -> Endpoint: 237 | """ 238 | Returns an Endpoint object that represents the 239 | /apps/{app_id}/network/custom endpoint. 240 | """ 241 | return cls('CUSTOM_DOMAIN') 242 | 243 | @classmethod 244 | def all_backups(cls) -> Endpoint: 245 | """ 246 | Returns an Endpoint object that represents the 247 | /apps/{app_id}/backups endpoint. 248 | """ 249 | return cls('ALL_BACKUPS') 250 | 251 | @classmethod 252 | def all_apps_status(cls) -> Endpoint: 253 | """ 254 | Returns an Endpoint object that represents the 255 | /apps/status endpoint. 256 | """ 257 | return cls('ALL_APPS_STATUS') 258 | 259 | @classmethod 260 | def current_integration(cls) -> Endpoint: 261 | """ 262 | Returns an Endpoint object that represents the 263 | /apps/{app_id}/deployments/current endpoint. 264 | """ 265 | return cls('CURRENT_INTEGRATION') 266 | 267 | @classmethod 268 | def move_file(cls) -> Endpoint: 269 | """ 270 | Returns an Endpoint object that represents the 271 | /apps/{app_id}/files 272 | """ 273 | return cls('MOVE_FILE') 274 | 275 | @classmethod 276 | def dns_records(cls) -> Endpoint: 277 | """ 278 | Returns an Endpoint object that represents the 279 | /apps/{app_id}/network/dns 280 | """ 281 | return cls('DNSRECORDS') 282 | 283 | 284 | # pylint: disable=too-few-public-methods 285 | class Router: 286 | """Represents a route""" 287 | 288 | # BASE_V1: str = 'https://api.squarecloud.app/v1/public' 289 | BASE_V2: str = 'https://api.squarecloud.app/v2' 290 | 291 | # noinspection StrFormat 292 | def __init__(self, endpoint: Endpoint, **params: str | int) -> None: 293 | """ 294 | The __init__ function is called when the class is instantiated. 295 | It sets up the instance of the class, and it's where you define your 296 | attributes. 297 | 298 | 299 | :param self: Represent the instance of the class 300 | :param endpoint: Endpoint: Define the endpoint 301 | :param **params: Pass in the parameters for the url 302 | :return: None 303 | """ 304 | self.endpoint: Endpoint = endpoint 305 | self.method: str = endpoint.method 306 | self.path: str = endpoint.path 307 | url: str = self.BASE_V2 + self.path.format(**params) 308 | if params: 309 | url.format(params) 310 | self.url = url 311 | 312 | def __repr__(self) -> str: 313 | """ 314 | The __repr__ function is used to generate a string representation of 315 | an object. 316 | This function should return a printable representation of the object, 317 | and it will be used whenever you call str() on that object. 318 | 319 | :param self: Represent the instance of the class 320 | :return: A string that is a valid python expression 321 | """ 322 | return f"{Router.__name__}(path='{self.path}', method='{self.method}')" 323 | -------------------------------------------------------------------------------- /squarecloud/http/http_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import Any, Literal 5 | 6 | import aiohttp 7 | 8 | from squarecloud.file import File 9 | 10 | from ..errors import ( 11 | AuthenticationFailure, 12 | BadMemory, 13 | BadRequestError, 14 | FewMemory, 15 | InvalidAccessToken, 16 | InvalidDisplayName, 17 | InvalidDomain, 18 | InvalidMain, 19 | InvalidMemory, 20 | InvalidStart, 21 | InvalidVersion, 22 | MissingConfigFile, 23 | MissingDependenciesFile, 24 | MissingDisplayName, 25 | MissingMainFile, 26 | MissingMemory, 27 | MissingVersion, 28 | NotFoundError, 29 | RequestError, 30 | TooManyRequests, 31 | ) 32 | from ..logger import logger 33 | from .endpoints import Endpoint, Router 34 | 35 | 36 | class Response: 37 | """Represents a request response""" 38 | 39 | def __init__(self, data: dict[str, Any], route: Router) -> None: 40 | """ 41 | The __init__ function is called when the class is instantiated. 42 | It sets up the instance of the class, and defines all of its 43 | attributes. 44 | 45 | 46 | :param self: Represent the instance of the class 47 | :param data: RawResponseData: Pass the data from the response to this 48 | class 49 | :param route: Router: Store the route of the request 50 | :return: None 51 | """ 52 | self.data = data 53 | self.route: Router = route 54 | self.status: Literal['success', 'error'] = data['status'] 55 | self.code: int | None = data.get('code') 56 | self.message: str | None = data.get('message') 57 | self.response: dict[str, Any] | list[Any] = data.get('response', {}) 58 | 59 | def __repr__(self) -> str: 60 | """ 61 | The __repr__ function is used to compute the string representation of 62 | an object. 63 | 64 | :param self: Refer to the instance of the class 65 | :return: The name of the class and the data of the response 66 | """ 67 | return f'{Response.__name__}({self.data})' 68 | 69 | 70 | def _get_error(code: str) -> type[RequestError] | None: 71 | """ 72 | The _get_error function is a helper function that takes in an error code 73 | and returns the corresponding error class. 74 | This allows us to easily map errors to their respective classes, 75 | which makes it easier for us to raise them when we need to. 76 | 77 | :param code: str: Determine which error to raise 78 | :return: An error class 79 | :doc-author: Trelent 80 | """ 81 | errors = { 82 | 'FEW_MEMORY': FewMemory, 83 | 'BAD_MEMORY': BadMemory, 84 | 'MISSING_CONFIG': MissingConfigFile, 85 | 'MISSING_DEPENDENCIES_FILE': MissingDependenciesFile, 86 | 'MISSING_MAIN': MissingMainFile, 87 | 'INVALID_MAIN': InvalidMain, 88 | 'INVALID_DISPLAY_NAME': InvalidDisplayName, 89 | 'MISSING_DISPLAY_NAME': MissingDisplayName, 90 | 'INVALID_MEMORY': InvalidMemory, 91 | 'MISSING_MEMORY': MissingMemory, 92 | 'INVALID_VERSION': InvalidVersion, 93 | 'MISSING_VERSION': MissingVersion, 94 | 'INVALID_ACCESS_TOKEN': InvalidAccessToken, 95 | 'REGEX_VALIDATION': InvalidDomain, 96 | 'INVALID_START': InvalidStart, 97 | } 98 | error_class = errors.get(code, None) 99 | if error_class is None: 100 | return None 101 | return error_class 102 | 103 | 104 | class HTTPClient: 105 | """A client that handles requests and responses""" 106 | 107 | def __init__(self, api_key: str) -> None: 108 | """ 109 | The __init__ function is called when the class is instantiated. 110 | It sets up the class with all of its attributes and other things it 111 | needs to function properly. 112 | 113 | :param self: Represent the instance of the class 114 | :param api_key: str: Store the api key that is passed in when the 115 | class is instantiated 116 | :return: None 117 | """ 118 | self.api_key = api_key 119 | self.__session = aiohttp.ClientSession 120 | self._last_response: Response | None = None 121 | 122 | async def request(self, route: Router, **kwargs: Any) -> Response: 123 | """ 124 | Sends a request to the Square API and returns the response. 125 | 126 | :param route: the route to send a request 127 | :param kwargs: Keyword arguments 128 | :return: A Response object 129 | :rtype: Response 130 | 131 | :raises NotFoundError: Raised when the request status code is 404 132 | :raises BadRequestError: Raised when the request status code is 400 133 | :raises AuthenticationFailure: Raised when the request status 134 | code is 401 135 | :raises TooManyRequestsError: Raised when the request status 136 | code is 429 137 | :raises FewMemory: Raised when user memory reached the maximum 138 | amount of memory 139 | :raises BadMemory: Raised when the memory in configuration file is 140 | invalid 141 | :raises MissingConfigFile: Raised when the .zip file is missing the 142 | config file (squarecloud.app/squarecloud.config) 143 | :raises MissingDependenciesFile: Raised when the .zip file is missing 144 | the dependencies file (requirements.txt, package.json, ...) 145 | :raises MissingMainFile: Raised when the .zip file is missing the main 146 | file (main.py, index.js, ...) 147 | :raises InvalidMain: Raised when the field MAIN in config file is 148 | invalid or when the main file is corrupted 149 | :raises InvalidDisplayName: Raised when the field DISPLAY_NAME 150 | in config file is invalid 151 | :raises MissingDisplayName: Raised when the DISPLAY_NAME field is 152 | missing in the config file 153 | :raises InvalidMemory: Raised when the MEMORY field is invalid 154 | :raises MissingMemory: Raised when the MEMORY field is missing in 155 | the config file 156 | :raises InvalidVersion: Raised when the VERSION field is invalid, 157 | the value accepted is "recommended" or "latest" 158 | :raises MissingVersion: Raised when the VERSION field is missing in 159 | the config file 160 | :raises InvalidAccessToken: Raised when a GitHub access token 161 | provided is invalid 162 | :raises InvalidDomain: Raised when a domain provided is invalid 163 | """ 164 | headers = { 165 | 'Authorization': self.api_key, 166 | 'User-Agent': 'squarecloud-api/3.5.1', 167 | } 168 | extra_error_kwargs: dict[str, Any] = {} 169 | 170 | if kwargs.get('custom_domain'): 171 | extra_error_kwargs['domain'] = kwargs.pop('custom_domain') 172 | 173 | if route.endpoint in (Endpoint.commit(), Endpoint.upload()): 174 | file: File = kwargs.pop('file') 175 | form = aiohttp.FormData() 176 | form.add_field('file', file.bytes, filename=file.filename) 177 | kwargs['data'] = form 178 | async with self.__session(headers=headers) as session: 179 | async with session.request( 180 | url=route.url, method=route.method, **kwargs 181 | ) as resp: 182 | status_code = resp.status 183 | data: dict[str, Any] = await resp.json() 184 | response = Response(data=data, route=route) 185 | self._last_response = response 186 | 187 | code: str | None = data.get('code') 188 | error: type[RequestError] = RequestError 189 | log_msg = '{status} request to route: {route}' 190 | log_msg = log_msg.format( 191 | status=data.get('status'), 192 | route=route.url, 193 | ) 194 | 195 | if code: 196 | log_msg += f' with code: {code}' 197 | log_level: int 198 | 199 | match status_code: 200 | case 200: 201 | log_level = logging.DEBUG 202 | case 404: 203 | if code is None: 204 | log_level = logging.DEBUG 205 | else: 206 | log_level = logging.ERROR 207 | error = NotFoundError 208 | case 400: 209 | log_level = logging.ERROR 210 | error = BadRequestError 211 | case 401: 212 | log_level = logging.ERROR 213 | error = AuthenticationFailure 214 | case 429: 215 | log_level = logging.ERROR 216 | error = TooManyRequests 217 | case _: 218 | log_level = logging.ERROR 219 | error = RequestError 220 | if code: 221 | if _ := _get_error(code): 222 | log_level = logging.ERROR 223 | error = _ 224 | logger.log(log_level, log_msg, extra={'type': 'http'}) 225 | raise error( 226 | **extra_error_kwargs, 227 | route=route.endpoint.name, 228 | status_code=status_code, 229 | code=code, 230 | ) 231 | return response 232 | 233 | @classmethod 234 | async def fetch_backup_content(cls, url: str) -> bytes: 235 | async with aiohttp.ClientSession() as session: 236 | async with session.get(url) as response: 237 | return await response.read() 238 | 239 | async def fetch_user_info(self) -> Response: 240 | """ 241 | Fetches user information and returns the response object 242 | 243 | :return: A Response object 244 | :rtype: Response 245 | 246 | :raises BadRequestError: Raised when the request status code is 400 247 | :raises AuthenticationFailure: Raised when the request status 248 | code is 401 249 | :raises TooManyRequestsError: Raised when the request status 250 | code is 429 251 | """ 252 | route = Router(Endpoint.user()) 253 | response: Response = await self.request(route) 254 | return response 255 | 256 | async def fetch_app_status(self, app_id: str) -> Response: 257 | """ 258 | Fetches status of a hosted application 259 | 260 | :param app_id: The application id 261 | :return: A Response object 262 | :rtype: Response 263 | 264 | :raises NotFoundError: Raised when the request status code is 404 265 | :raises BadRequestError: Raised when the request status code is 400 266 | :raises AuthenticationFailure: Raised when the request status 267 | code is 401 268 | :raises TooManyRequestsError: Raised when the request status 269 | code is 429 270 | """ 271 | route: Router = Router(Endpoint.app_status(), app_id=app_id) 272 | response: Response = await self.request(route) 273 | return response 274 | 275 | async def fetch_logs(self, app_id: str) -> Response | None: 276 | """ 277 | Fetches logs of a hosted application 278 | 279 | :param app_id: The application id 280 | :return: A Response object or None 281 | 282 | :rtype: Response | None 283 | 284 | :raises NotFoundError: Raised when the request status code is 404 285 | :raises BadRequestError: Raised when the request status code is 400 286 | :raises AuthenticationFailure: Raised when the request status 287 | code is 401 288 | :raises TooManyRequestsError: Raised when the request status 289 | code is 429 290 | """ 291 | route: Router = Router(Endpoint.logs(), app_id=app_id) 292 | return await self.request(route) 293 | 294 | async def start_application(self, app_id: str) -> Response: 295 | """ 296 | Start a hosted application 297 | 298 | :param app_id: The application id 299 | :return: A Response object 300 | :rtype: Response 301 | 302 | :raises NotFoundError: Raised when the request status code is 404 303 | :raises BadRequestError: Raised when the request status code is 400 304 | :raises AuthenticationFailure: Raised when the request status 305 | code is 401 306 | :raises TooManyRequestsError: Raised when the request status 307 | code is 429 308 | """ 309 | route: Router = Router(Endpoint.start(), app_id=app_id) 310 | response: Response = await self.request(route) 311 | return response 312 | 313 | async def stop_application(self, app_id: str) -> Response: 314 | """ 315 | Stop a hosted application 316 | 317 | :param app_id: The application id 318 | :return: A Response object 319 | :rtype: Response 320 | 321 | :raises NotFoundError: Raised when the request status code is 404 322 | :raises BadRequestError: Raised when the request status code is 400 323 | :raises AuthenticationFailure: Raised when the request status 324 | code is 401 325 | :raises TooManyRequestsError: Raised when the request status 326 | code is 429 327 | """ 328 | route: Router = Router(Endpoint.stop(), app_id=app_id) 329 | response: Response = await self.request(route) 330 | return response 331 | 332 | async def restart_application(self, app_id: str) -> Response: 333 | """ 334 | Restart a hosted application 335 | 336 | :param app_id: The application id 337 | :return: A Response object 338 | :rtype: Response 339 | 340 | :raises NotFoundError: Raised when the request status code is 404 341 | :raises BadRequestError: Raised when the request status code is 400 342 | :raises AuthenticationFailure: Raised when the request status 343 | code is 401 344 | :raises TooManyRequestsError: Raised when the request status 345 | code is 429 346 | """ 347 | route: Router = Router(Endpoint.restart(), app_id=app_id) 348 | response: Response = await self.request(route) 349 | return response 350 | 351 | async def backup(self, app_id: str) -> Response: 352 | """ 353 | Backup a hosted application 354 | 355 | :param app_id: The application id 356 | :return: A Response object 357 | :rtype: Response 358 | 359 | :raises NotFoundError: Raised when the request status code is 404 360 | :raises BadRequestError: Raised when the request status code is 400 361 | :raises AuthenticationFailure: Raised when the request status 362 | code is 401 363 | :raises TooManyRequestsError: Raised when the request status 364 | code is 429 365 | """ 366 | route: Router = Router(Endpoint.backup(), app_id=app_id) 367 | response: Response = await self.request(route) 368 | return response 369 | 370 | async def delete_application(self, app_id: str) -> Response: 371 | """ 372 | Delete a hosted application 373 | 374 | :param app_id: The application id 375 | :return: A Response object 376 | :rtype: Response 377 | 378 | :raises NotFoundError: Raised when the request status code is 404 379 | :raises BadRequestError: Raised when the request status code is 400 380 | :raises AuthenticationFailure: Raised when the request status 381 | code is 401 382 | :raises TooManyRequestsError: Raised when the request status 383 | code is 429 384 | """ 385 | route: Router = Router(Endpoint.delete_app(), app_id=app_id) 386 | response: Response = await self.request(route) 387 | return response 388 | 389 | async def commit(self, app_id: str, file: File) -> Response: 390 | """ 391 | Commit a file to an application 392 | 393 | :param app_id: The application id 394 | :param file: A File object to be committed 395 | :return: A Response object 396 | :rtype: Response 397 | 398 | :raises NotFoundError: Raised when the request status code is 404 399 | :raises BadRequestError: Raised when the request status code is 400 400 | :raises AuthenticationFailure: Raised when the request status 401 | code is 401 402 | :raises TooManyRequestsError: Raised when the request status 403 | code is 429 404 | """ 405 | route: Router = Router(Endpoint.commit(), app_id=app_id) 406 | response: Response = await self.request(route, file=file) 407 | return response 408 | 409 | async def upload(self, file: File) -> Response: 410 | """ 411 | Upload a new application 412 | 413 | :param file: A File object to be uploaded 414 | :return: A Response object 415 | :rtype: Response 416 | 417 | :raises NotFoundError: Raised when the request status code is 404 418 | :raises BadRequestError: Raised when the request status code is 400 419 | :raises AuthenticationFailure: Raised when the request status 420 | code is 401 421 | :raises TooManyRequestsError: Raised when the request status 422 | code is 429 423 | :raises FewMemory: Raised when user memory reached the maximum 424 | amount of memory 425 | :raises BadMemory: Raised when the memory in configuration file is 426 | invalid 427 | :raises MissingConfigFile: Raised when the .zip file is missing the 428 | config file (squarecloud.app/squarecloud.config) 429 | :raises MissingDependenciesFile: Raised when the .zip file is missing 430 | the dependencies file (requirements.txt, package.json, ...) 431 | :raises MissingMainFile: Raised when the .zip file is missing the main 432 | file (main.py, index.js, ...) 433 | :raises InvalidMain: Raised when the field MAIN in config file is 434 | invalid or when the main file is corrupted 435 | :raises InvalidDisplayName: Raised when the field DISPLAY_NAME 436 | in config file is invalid 437 | :raises MissingDisplayName: Raised when the DISPLAY_NAME field is 438 | missing in the config file 439 | :raises InvalidMemory: Raised when the MEMORY field is invalid 440 | :raises MissingMemory: Raised when the MEMORY field is missing in 441 | the config file 442 | :raises InvalidVersion: Raised when the VERSION field is invalid, 443 | the value accepted is "recommended" or "latest" 444 | :raises MissingVersion: Raised when the VERSION field is missing in 445 | the config file 446 | :raises InvalidAccessToken: Raised when a GitHub access token 447 | provided is invalid 448 | :raises InvalidDomain: Raised when a domain provided is invalid 449 | """ 450 | route: Router = Router(Endpoint.upload()) 451 | response: Response = await self.request(route, file=file) 452 | return response 453 | 454 | async def fetch_app_files_list(self, app_id: str, path: str) -> Response: 455 | """ 456 | Fetches the files list of the application 457 | 458 | :param app_id: The application id 459 | request 460 | :param path: Specify the directory path 461 | :return: A Response object 462 | :rtype: Response 463 | 464 | :raises NotFoundError: Raised when the request status code is 404 465 | :raises BadRequestError: Raised when the request status code is 400 466 | :raises AuthenticationFailure: Raised when the request status 467 | code is 401 468 | :raises TooManyRequestsError: Raised when the request status 469 | code is 429 470 | """ 471 | route: Router = Router(Endpoint.files_list(), app_id=app_id, path=path) 472 | response: Response = await self.request(route) 473 | return response 474 | 475 | async def read_app_file(self, app_id: str, path: str) -> Response: 476 | """ 477 | The read_app_file function reads the contents of a file in an app. 478 | 479 | :param app_id: The application id 480 | :param path: Specify the path of the file to be read 481 | :return: A Response object 482 | :rtype: Response 483 | 484 | :raises NotFoundError: Raised when the request status code is 404 485 | :raises BadRequestError: Raised when the request status code is 400 486 | :raises AuthenticationFailure: Raised when the request status 487 | code is 401 488 | :raises TooManyRequestsError: Raised when the request status 489 | code is 429 490 | """ 491 | route: Router = Router(Endpoint.files_read(), app_id=app_id, path=path) 492 | response: Response = await self.request(route) 493 | return response 494 | 495 | async def create_app_file( 496 | self, app_id: str, file: list[bytes], path: str 497 | ) -> Response: 498 | """ 499 | The create_app_file method creates a file in the specified app. 500 | 501 | :param app_id: The application id 502 | :param file: Specify the file to be uploaded 503 | :param path: str: Specify the path of the file 504 | :return: A Response object 505 | 506 | :raises NotFoundError: Raised when the request status code is 404 507 | :raises BadRequestError: Raised when the request status code is 400 508 | :raises AuthenticationFailure: Raised when the request status 509 | code is 401 510 | :raises TooManyRequestsError: Raised when the request status 511 | code is 429 512 | """ 513 | route: Router = Router(Endpoint.files_create(), app_id=app_id) 514 | response: Response = await self.request( 515 | route, json={'buffer': file, 'path': '/' + path} 516 | ) 517 | return response 518 | 519 | async def file_delete(self, app_id: str, path: str) -> Response: 520 | """ 521 | The file_delete method deletes a file from the application. 522 | 523 | :param app_id: The application id 524 | :param path: Specify the path of the file to be deleted 525 | :return: A Response object 526 | :rtype: Response 527 | 528 | :raises NotFoundError: Raised when the request status code is 404 529 | :raises BadRequestError: Raised when the request status code is 400 530 | :raises AuthenticationFailure: Raised when the request status 531 | code is 401 532 | :raises TooManyRequestsError: Raised when the request status 533 | code is 429 534 | """ 535 | route: Router = Router( 536 | Endpoint.files_delete(), 537 | app_id=app_id, 538 | ) 539 | body = {'path': path} 540 | response: Response = await self.request(route, json=body) 541 | return response 542 | 543 | async def get_app_data(self, app_id: str) -> Response: 544 | """ 545 | The get_app_data method returns a Response object containing the 546 | app data for the specified app_id. 547 | 548 | :param app_id: The application id 549 | :return: A Response object 550 | :rtype: Response 551 | 552 | :raises NotFoundError: Raised when the request status code is 404 553 | :raises BadRequestError: Raised when the request status code is 400 554 | :raises AuthenticationFailure: Raised when the request status 555 | code is 401 556 | :raises TooManyRequestsError: Raised when the request status 557 | code is 429 558 | """ 559 | route: Router = Router(Endpoint.app_data(), app_id=app_id) 560 | response: Response = await self.request(route) 561 | return response 562 | 563 | async def get_last_deploys(self, app_id: str) -> Response: 564 | """ 565 | The get_last_deploys method returns the last deploys of an 566 | application. 567 | 568 | :param app_id: The application id 569 | :return: A Response object 570 | :rtype: Response 571 | 572 | :raises NotFoundError: Raised when the request status code is 404 573 | :raises BadRequestError: Raised when the request status code is 400 574 | :raises AuthenticationFailure: Raised when the request status 575 | code is 401 576 | :raises TooManyRequestsError: Raised when the request status 577 | code is 429 578 | """ 579 | route: Router = Router(Endpoint.last_deploys(), app_id=app_id) 580 | response: Response = await self.request(route) 581 | return response 582 | 583 | async def create_github_integration( 584 | self, app_id: str, github_access_token: str 585 | ) -> Response: 586 | """ 587 | The create_github_integration method returns a webhook to integrate 588 | with a GitHub repository. 589 | 590 | :param app_id: The application id 591 | :param github_access_token: GitHub access token 592 | :return: A Response object 593 | :rtype: Response 594 | 595 | :raises InvalidAccessToken: Raised when a GitHub access token 596 | provided is invalid 597 | :raises NotFoundError: Raised when the request status code is 404 598 | :raises BadRequestError: Raised when the request status code is 400 599 | :raises AuthenticationFailure: Raised when the request status 600 | code is 401 601 | :raises TooManyRequestsError: Raised when the request status 602 | code is 429 603 | """ 604 | route: Router = Router(Endpoint.github_integration(), app_id=app_id) 605 | body = {'access_token': github_access_token} 606 | response: Response = await self.request(route, json=body) 607 | return response 608 | 609 | async def update_custom_domain( 610 | self, app_id: str, custom_domain: str 611 | ) -> Response: 612 | """ 613 | The update_custom_domain method updates the custom domain of an app. 614 | 615 | :param app_id: The application id 616 | :param custom_domain: Set the custom domain for the app 617 | :return: A Response object 618 | :rtype: Response 619 | 620 | :raises InvalidDomain: Raised when a domain provided is invalid 621 | :raises NotFoundError: Raised when the request status code is 404 622 | :raises BadRequestError: Raised when the request status code is 400 623 | :raises AuthenticationFailure: Raised when the request status 624 | code is 401 625 | :raises TooManyRequestsError: Raised when the request status 626 | code is 429 627 | """ 628 | route: Router = Router( 629 | Endpoint.custom_domain(), 630 | app_id=app_id, 631 | custom_domain=custom_domain, 632 | ) 633 | response: Response = await self.request( 634 | route, custom_domain=custom_domain 635 | ) 636 | return response 637 | 638 | async def domain_analytics(self, app_id: str) -> Response: 639 | """ 640 | The domain_analytics method returns a list of all domains analytics 641 | for the specified app. 642 | 643 | :param app_id: The application id 644 | :return: A Response object 645 | :rtype: Response 646 | 647 | :raises NotFoundError: Raised when the request status code is 404 648 | :raises BadRequestError: Raised when the request status code is 400 649 | :raises AuthenticationFailure: Raised when the request status 650 | code is 401 651 | :raises TooManyRequestsError: Raised when the request status 652 | code is 429 653 | """ 654 | route: Router = Router(Endpoint.domain_analytics(), app_id=app_id) 655 | response: Response = await self.request(route) 656 | return response 657 | 658 | async def get_all_app_backups(self, app_id: str) -> Response: 659 | """ 660 | Returns a list of all backups of the specified application 661 | 662 | :return: A Response object 663 | :rtype: Response 664 | 665 | :raises NotFoundError: Raised when the request status code is 404 666 | :raises BadRequestError: Raised when the request status code is 400 667 | :raises AuthenticationFailure: Raised when the request status 668 | code is 401 669 | :raises TooManyRequestsError: Raised when the request status 670 | code is 429 671 | """ 672 | route: Router = Router(Endpoint.all_backups(), app_id=app_id) 673 | response: Response = await self.request(route) 674 | return response 675 | 676 | async def all_apps_status(self) -> Response: 677 | """ 678 | Returns all applications status 679 | 680 | :return: A Response object 681 | :rtype: Response 682 | 683 | :raises NotFoundError: Raised when the request status code is 404 684 | :raises BadRequestError: Raised when the request status code is 400 685 | :raises AuthenticationFailure: Raised when the request status 686 | code is 401 687 | :raises TooManyRequestsError: Raised when the request status 688 | code is 429 689 | """ 690 | route: Router = Router(Endpoint.all_apps_status()) 691 | response: Response = await self.request(route) 692 | return response 693 | 694 | async def move_app_file( 695 | self, app_id: str, origin: str, dest: str 696 | ) -> Response: 697 | """ 698 | Make a http request to move an app file. 699 | 700 | :return: A Response object 701 | :rtype: Response 702 | 703 | :raises NotFoundError: Raised when the request status code is 404 704 | :raises BadRequestError: Raised when the request status code is 400 705 | :raises AuthenticationFailure: Raised when the request status 706 | code is 401 707 | :raises TooManyRequestsError: Raised when the request status 708 | code is 429 709 | """ 710 | route: Router = Router(Endpoint.move_file(), app_id=app_id) 711 | body = {'path': origin, 'to': dest} 712 | response: Response = await self.request(route, json=body) 713 | return response 714 | 715 | async def dns_records(self, app_id: str) -> Response: 716 | """ 717 | Returns dns information of the specified application 718 | 719 | :return: A Response object 720 | :rtype: Response 721 | 722 | :raises NotFoundError: Raised when the request status code is 404 723 | :raises BadRequestError: Raised when the request status code is 400 724 | :raises AuthenticationFailure: Raised when the request status 725 | code is 401 726 | :raises TooManyRequestsError: Raised when the request status 727 | code is 429 728 | """ 729 | route: Router = Router(Endpoint.dns_records(), app_id=app_id) 730 | response: Response = await self.request(route) 731 | return response 732 | 733 | async def get_app_current_integration(self, app_id: str) -> Response: 734 | """ 735 | Returns the webhook url of the application current integration 736 | 737 | :return: A Response object 738 | :rtype: Response 739 | 740 | :raises NotFoundError: Raised when the request status code is 404 741 | :raises BadRequestError: Raised when the request status code is 400 742 | :raises AuthenticationFailure: Raised when the request status 743 | code is 401 744 | :raises TooManyRequestsError: Raised when the request status 745 | code is 429 746 | """ 747 | route: Router = Router(Endpoint.current_integration(), app_id=app_id) 748 | response: Response = await self.request(route) 749 | return response 750 | 751 | @property 752 | def last_response(self) -> Response | None: 753 | """ 754 | Returns the last response made 755 | 756 | :return: A Response object or None 757 | :rtype: Response | None 758 | """ 759 | return self._last_response 760 | -------------------------------------------------------------------------------- /squarecloud/listeners/__init__.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import types 3 | from dataclasses import dataclass 4 | from types import MappingProxyType 5 | from typing import TYPE_CHECKING, Any, Callable, Optional, Type, Union 6 | 7 | from .._internal.constants import USING_PYDANTIC 8 | 9 | if USING_PYDANTIC: 10 | import pydantic 11 | from pydantic import BaseModel 12 | 13 | from .. import data, errors 14 | from ..http.endpoints import Endpoint 15 | 16 | if TYPE_CHECKING: 17 | from ..app import Application 18 | from ..client import Client 19 | 20 | 21 | @dataclass(frozen=False) 22 | class ListenerConfig: 23 | force_raise: bool = False 24 | 25 | 26 | class Listener: 27 | __slots__ = ( 28 | '_app', 29 | '_client', 30 | '_endpoint', 31 | '_callback', 32 | '_callback_params', 33 | 'config', 34 | ) 35 | 36 | def __init__( 37 | self, 38 | endpoint: Endpoint, 39 | callback: Callable, 40 | config: ListenerConfig = ListenerConfig(), 41 | app: Optional['Application'] = None, 42 | client: Optional['Client'] = None, 43 | ) -> None: 44 | self._app = app 45 | self._client = client 46 | self._endpoint = endpoint 47 | self._callback = callback 48 | self._callback_params = inspect.signature(callback).parameters 49 | self.config = config 50 | 51 | @property 52 | def app(self) -> 'Application': 53 | return self._app 54 | 55 | @property 56 | def endpoint(self) -> Endpoint: 57 | return self._endpoint 58 | 59 | @property 60 | def callback(self) -> Callable: 61 | return self._callback 62 | 63 | @property 64 | def callback_params(self) -> MappingProxyType[str, inspect.Parameter]: 65 | return self._callback_params 66 | 67 | def __repr__(self) -> None: 68 | return f'{self.__class__.__name__}(endpoint={self.endpoint})' 69 | 70 | 71 | class ListenerManager: 72 | def __init__(self) -> None: 73 | """ 74 | The __init__ method is called when the class is instantiated. 75 | It sets up the instance variables that will be used by other methods 76 | in the class. 77 | 78 | 79 | :param self: Refer to the class instance 80 | :return: A dictionary of the capture listeners and request listeners 81 | """ 82 | self.listeners: dict[str, Listener] = {} 83 | 84 | def get_listener(self, endpoint: Endpoint) -> Listener | None: 85 | """ 86 | The get_listener method is used to get the capture listener 87 | for a given endpoint. 88 | 89 | :param self: Refer to the class instance 90 | :param endpoint: Endpoint: Get the capture listener from the endpoint 91 | name 92 | :return: The capture listener for the given endpoint 93 | """ 94 | return self.listeners.get(endpoint.name) 95 | 96 | def include_listener(self, listener: Listener) -> Listener: 97 | """ 98 | The include_listener method adds a listener to the 99 | capture_listeners dictionary. 100 | The key is the name of an endpoint, and the value is a callable 101 | function that will be called when 102 | the endpoint's data has been captured. 103 | 104 | :param listener: the listener that will be included 105 | :param self: Refer to the class instance 106 | listen to 107 | request is made to the endpoint 108 | :return: None 109 | :raises InvalidListener: Raised if the endpoint is already registered 110 | """ 111 | if self.get_listener(listener.endpoint): 112 | raise errors.InvalidListener( 113 | message='Already exists an capture_listener for ' 114 | f'{listener.endpoint}', 115 | listener=listener.callback, 116 | ) 117 | self.listeners.update({listener.endpoint.name: listener}) 118 | return listener 119 | 120 | def remove_listener(self, endpoint: Endpoint) -> Callable: 121 | """ 122 | The remove_listener method removes a capture listener from 123 | the list of listeners. 124 | 125 | :param self: Refer to the class instance 126 | :param endpoint: Endpoint: Identify the endpoint to remove 127 | :return: The capture_listener that was removed from the dictionary 128 | """ 129 | if self.get_listener(endpoint): 130 | self.listeners.pop(endpoint.name) 131 | 132 | def clear_listeners(self) -> None: 133 | """ 134 | The clear_listeners function clears the capture_listeners list. 135 | 136 | :param self: Refer to the class instance 137 | :return: None 138 | """ 139 | self.listeners = None 140 | 141 | @classmethod 142 | def cast_to_pydantic_model( 143 | cls, model: Type['BaseModel'], values: dict[Any, Any] 144 | ) -> None: 145 | result: BaseModel | None | dict = values 146 | if isinstance(model, types.UnionType): 147 | for ty in model.__args__: 148 | if ty is None: 149 | continue 150 | if not issubclass(ty, BaseModel): 151 | continue 152 | try: 153 | return ty(**values) 154 | except pydantic.ValidationError: 155 | continue 156 | return None 157 | if issubclass(model, BaseModel): 158 | try: 159 | result = model(**values) 160 | except pydantic.ValidationError as e: 161 | print(e) 162 | result = None 163 | return result 164 | 165 | 166 | ListenerDataTypes = Union[ 167 | data.AppData, 168 | data.StatusData, 169 | data.LogsData, 170 | data.Backup, 171 | ] 172 | -------------------------------------------------------------------------------- /squarecloud/listeners/capture_listener.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | import types 4 | from typing import Any, Union 5 | 6 | from .. import data, errors 7 | from .._internal.constants import USING_PYDANTIC 8 | from ..http import Endpoint 9 | from . import Listener, ListenerManager 10 | 11 | if USING_PYDANTIC: 12 | import pydantic 13 | 14 | ListenerDataTypes = Union[ 15 | data.AppData, 16 | data.StatusData, 17 | data.LogsData, 18 | data.Backup, 19 | ] 20 | 21 | 22 | class CaptureListenerManager(ListenerManager): 23 | """CaptureListenerManager""" 24 | 25 | def __init__(self) -> None: 26 | """ 27 | The __init__ function is called when the class is instantiated. 28 | It sets up the instance variables that will be used by other methods 29 | in the class. 30 | 31 | 32 | :param self: Refer to the class instance 33 | :return: A dictionary of the capture listeners and request listeners 34 | """ 35 | super().__init__() 36 | 37 | def include_listener(self, listener: Listener) -> Listener: 38 | allowed_endpoints: tuple[Endpoint, Endpoint, Endpoint, Endpoint] = ( 39 | Endpoint.logs(), 40 | Endpoint.app_status(), 41 | Endpoint.backup(), 42 | Endpoint.app_data(), 43 | ) 44 | 45 | if listener.endpoint not in allowed_endpoints: 46 | raise errors.InvalidListener( 47 | message='the endpoint to capture must be ' 48 | f'{allowed_endpoints}', 49 | listener=listener.callback, 50 | ) 51 | if self.get_listener(listener.endpoint): 52 | raise errors.InvalidListener( 53 | message='Already exists an capture_listener for ' 54 | f'{listener.endpoint}', 55 | listener=listener.callback, 56 | ) 57 | self.listeners.update({listener.endpoint.name: listener}) 58 | return listener 59 | 60 | async def notify( 61 | self, 62 | endpoint: Endpoint, 63 | before: ListenerDataTypes | None, 64 | after: ListenerDataTypes, 65 | extra_value: Any = None, 66 | ) -> Any: 67 | """ 68 | The on_capture function is called when a capture event occurs. 69 | 70 | :param self: Refer to the class instance 71 | :param endpoint: Endpoint: Get the endpoint that is being called 72 | :param before: 73 | :param after: 74 | :param extra: 75 | :return: The result of the call function 76 | """ 77 | 78 | def filter_annotations(annotations: list[Any]) -> Any: 79 | for item in annotations: 80 | if issubclass(item, pydantic.BaseModel): 81 | yield item 82 | 83 | if not (listener := self.get_listener(endpoint)): 84 | return None 85 | logger = logging.getLogger('squarecloud') 86 | kwargs: dict[str, Any] = {} 87 | call_params = listener.callback_params 88 | call_extra_param: inspect.Parameter | None = call_params.get('extra') 89 | extra_annotation: Any | None = None 90 | 91 | if 'before' in call_params.keys(): 92 | kwargs['before'] = before 93 | if 'after' in call_params.keys(): 94 | kwargs['after'] = after 95 | if 'extra' in call_params.keys(): 96 | kwargs['extra'] = extra_value 97 | info_msg: str = ( 98 | f'ENDPOINT: {listener.endpoint}\n' 99 | f'APP-TAG: {listener.app.name}\n' 100 | f'APP-ID: {listener.app.id}' 101 | ) 102 | if call_extra_param: 103 | info_msg += f'\nEXTRA: {extra_value}' 104 | extra_annotation = call_extra_param.annotation 105 | 106 | if ( 107 | call_extra_param is not None 108 | and extra_annotation is not None 109 | and extra_annotation != call_extra_param.empty 110 | ): 111 | cast_result = self.cast_to_pydantic_model( 112 | extra_annotation, extra_value 113 | ) 114 | if not cast_result: 115 | msg: str = ( 116 | f'a "{extra_annotation.__name__}"' 117 | if not isinstance(extra_annotation, types.UnionType) 118 | else str( 119 | [ 120 | x.__name__ 121 | for x in filter_annotations( 122 | list(extra_annotation.__args__) 123 | ) 124 | ] 125 | ) 126 | ) 127 | logger.warning( 128 | 'Failed on cast extra argument in ' 129 | f'"{listener.callback.__name__}" into ' 130 | f'{msg} pydantic model.\n' 131 | f'{info_msg}\n' 132 | f'The listener has been skipped.', 133 | extra={'type': 'listener'}, 134 | ) 135 | return None 136 | kwargs['extra'] = cast_result 137 | 138 | is_coro: bool = inspect.iscoroutinefunction(listener.callback) 139 | try: 140 | if is_coro: 141 | listener_result = await listener.callback(**kwargs) 142 | else: 143 | listener_result = listener.callback(**kwargs) 144 | logger.info( 145 | f'listener "{listener.callback.__name__}" was invoked.\n' 146 | f'{info_msg}\n' 147 | f'RETURN: {listener_result}', 148 | extra={'type': 'listener'}, 149 | ) 150 | return listener_result 151 | except Exception as exc: 152 | logger.error( 153 | f'Failed to call listener "{listener.callback.__name__}.\n' 154 | f'Error: {exc.__repr__()}.\n' 155 | f'APP-TAG: {listener.app.name}\n' 156 | f'APP-ID: {listener.app.id}', 157 | extra={'type': 'listener'}, 158 | ) 159 | if listener.config.force_raise: 160 | raise exc 161 | -------------------------------------------------------------------------------- /squarecloud/listeners/request_listener.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import logging 3 | import types 4 | from typing import Any 5 | 6 | from .._internal.constants import USING_PYDANTIC 7 | 8 | if USING_PYDANTIC: 9 | import pydantic 10 | 11 | from ..http import Endpoint, Response 12 | from . import ListenerManager 13 | 14 | 15 | class RequestListenerManager(ListenerManager): 16 | """CaptureListenerManager""" 17 | 18 | def __init__(self) -> None: 19 | """ 20 | The __init__ function is called when the class is instantiated. 21 | It sets up the instance variables that will be used by other methods 22 | in the class. 23 | 24 | 25 | :param self: Refer to the class instance 26 | :return: A dictionary of the capture listeners and request listeners 27 | """ 28 | super().__init__() 29 | 30 | async def notify( 31 | self, endpoint: Endpoint, response: Response, extra_value: Any 32 | ) -> Any: 33 | """ 34 | The on_request function is called when a request has been made to the 35 | endpoint. 36 | The response object contains all the information about the request 37 | 38 | :param self: Refer to the class instance 39 | :param endpoint: Endpoint: Get the endpoint that was called 40 | :param response: Response: Get the response from the endpoint 41 | :return: The result of the call function 42 | """ 43 | 44 | def filter_annotations(annotations: list[Any]) -> Any: 45 | for item in annotations: 46 | if issubclass(item, pydantic.BaseModel): 47 | yield item 48 | 49 | if not (listener := self.get_listener(endpoint)): 50 | return None 51 | logger = logging.getLogger('squarecloud') 52 | kwargs: dict[str, Any] = {} 53 | call_params = listener.callback_params 54 | call_extra_param: inspect.Parameter | None = call_params.get('extra') 55 | annotation: Any | None = None 56 | 57 | if 'response' in call_params.keys(): 58 | kwargs['response'] = response 59 | if 'extra' in call_params.keys(): 60 | kwargs['extra'] = extra_value 61 | 62 | if call_extra_param: 63 | annotation = call_extra_param.annotation 64 | if ( 65 | call_extra_param is not None 66 | and annotation is not None 67 | and annotation != call_extra_param.empty 68 | and USING_PYDANTIC 69 | ): 70 | annotation = call_extra_param.annotation 71 | cast_result = self.cast_to_pydantic_model(annotation, extra_value) 72 | if not cast_result: 73 | msg: str = ( 74 | f'a "{annotation.__name__}"' 75 | if not isinstance(annotation, types.UnionType) 76 | else str( 77 | [ 78 | x.__name__ 79 | for x in filter_annotations( 80 | list(annotation.__args__) 81 | ) 82 | ] 83 | ) 84 | ) 85 | logger.warning( 86 | 'Failed on cast extra argument in ' 87 | f'"{listener.callback.__name__}" into ' 88 | f'{msg}.\n' 89 | f'The listener has been skipped.', 90 | extra={'type': 'listener'}, 91 | ) 92 | return None 93 | kwargs['extra'] = cast_result 94 | is_coro: bool = inspect.iscoroutinefunction(listener.callback) 95 | try: 96 | if is_coro: 97 | listener_result = await listener.callback(**kwargs) 98 | else: 99 | listener_result = listener.callback(**kwargs) 100 | logger.info( 101 | f'listener "{listener.callback.__name__}" was invoked.\n' 102 | f'Endpoint: {listener.endpoint}\n' 103 | f'RETURN: {listener_result}\n', 104 | extra={'type': 'listener'}, 105 | ) 106 | return listener_result 107 | except Exception as exc: 108 | logger.error( 109 | f'Failed to call listener "{listener.callback.__name__}.\n' 110 | f'Error: {exc.__repr__()}.\n', 111 | extra={'type': 'listener'}, 112 | ) 113 | if listener.config.force_raise: 114 | raise exc 115 | -------------------------------------------------------------------------------- /squarecloud/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | GREEN = '\033[1;32m' 4 | BLUE = '\033[1;34m' 5 | PURPLE = '\033[1;35m' 6 | YELLOW = '\033[1;33m' 7 | RED = '\033[1;31m' 8 | END = '\033[m' 9 | 10 | 11 | class LogFormatter(logging.Formatter): 12 | """A custom logging formatter""" 13 | 14 | HTTP = '[%(levelname)s] - [HTTP] %(message)s' 15 | LISTENER = '[%(levelname)s] - [LISTENER] %(message)s' 16 | 17 | def format(self, record: logging.LogRecord) -> str: 18 | """ 19 | The format function is called by the logging system to format a log 20 | record. 21 | The function should return a string that will be used as the message 22 | of the log record. 23 | 24 | 25 | :param self: Refer to the class instance 26 | :param record: logging.LogRecord: Pass the log record to the format 27 | function 28 | :return: A string that will be used to format the log message 29 | """ 30 | format_body: str = '' 31 | 32 | match record.__dict__.get('type'): 33 | case 'http': 34 | format_body = self.HTTP 35 | case 'listener': 36 | format_body = self.LISTENER 37 | 38 | match record.levelname: 39 | case 'DEBUG': 40 | format_body = f'{GREEN}{format_body}{END}' 41 | case 'INFO': 42 | format_body = f'{BLUE}{format_body}{END}' 43 | case 'ERROR': 44 | format_body = f'{RED}{format_body}{END}' 45 | case 'WARNING': 46 | format_body = f'{YELLOW}{format_body}{END}' 47 | 48 | formatter = logging.Formatter( 49 | f'{PURPLE}[%(asctime)s]{END} ' + format_body 50 | ) 51 | return formatter.format(record) 52 | 53 | 54 | handler = logging.StreamHandler() 55 | handler.setFormatter(LogFormatter()) 56 | logger = logging.getLogger('squarecloud') 57 | logger.setLevel(logging.NOTSET) 58 | logger.addHandler(handler) 59 | -------------------------------------------------------------------------------- /squarecloud/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squarecloudofc/sdk-api-py/420a56f54518d00aff09fcd8e90c905bfb448e1f/squarecloud/py.typed -------------------------------------------------------------------------------- /squarecloud/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Literal 2 | 3 | from squarecloud._internal.decorators import validate 4 | 5 | 6 | class ConfigFile: 7 | """ 8 | This object represents a configuration file. 9 | You can see more of this file in the official documentation: 10 | https://docs.squarecloud.app/articles/how-to-create-your-squarecloud-configuration-file 11 | """ 12 | 13 | @validate 14 | def __init__( 15 | self, 16 | display_name: str, 17 | main: str, 18 | memory: int, 19 | version: Literal['recommended', 'latest'] = 'recommended', 20 | description: str | None = None, 21 | subdomain: str | None = None, 22 | start: str | None = None, 23 | auto_restart: bool = False, 24 | ) -> None: 25 | if version not in ('latest', 'recommended'): 26 | raise ValueError('Invalid version') 27 | if memory < 256: 28 | raise ValueError('Memory must be greater than 256MB') 29 | if memory < 512 and subdomain: 30 | raise ValueError('Websites memory must be grater than 512MB') 31 | self.display_name = display_name 32 | self.main = main 33 | self.memory = memory 34 | self.version = version 35 | self.description = description 36 | self.subdomain = subdomain 37 | self.start = start 38 | self.auto_restart = auto_restart 39 | 40 | def __repr__(self) -> str: 41 | """Return a string representation of the config file content""" 42 | return f"""{self.__class__.__name__}(\n{self.content()}\n)""" 43 | 44 | @classmethod 45 | def from_str(cls, content: str) -> 'ConfigFile': 46 | """Returns a class from a file content string""" 47 | output: dict = {} 48 | for line in content.splitlines(): 49 | if '=' not in line: 50 | continue 51 | key, value = line.split('=') 52 | if value.isdigit(): 53 | value = int(value) 54 | output.update({key.lower(): value}) 55 | return cls(**output) 56 | 57 | @classmethod 58 | def from_dict(cls, dictionary: dict[str, Any]) -> 'ConfigFile': 59 | return cls(**dictionary) 60 | 61 | def to_dict(self) -> dict[str, Any]: 62 | return self.__dict__.copy() 63 | 64 | def content(self) -> str: 65 | content = '' 66 | for key, value in self.__dict__.items(): 67 | if value: 68 | string: str = f'{key.upper()}={value}\n' 69 | content += string 70 | return '\n'.join(content.splitlines()) 71 | 72 | def save(self, path: str = '') -> None: 73 | content = self.content() 74 | with open(f'./{path}/squarecloud.app', 'w', encoding='utf-8') as file: 75 | file.write(content) 76 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import zipfile 4 | 5 | from dotenv import load_dotenv 6 | 7 | from squarecloud import Client, Endpoint 8 | from squarecloud.app import Application 9 | from squarecloud.utils import ConfigFile 10 | 11 | load_dotenv() 12 | 13 | GITHUB_ACCESS_TOKEN: str = os.getenv('GITHUB_ACCESS_TOKEN') 14 | 15 | 16 | def create_zip(config: ConfigFile | str): 17 | buffer = io.BytesIO() 18 | if isinstance(config, ConfigFile): 19 | config = config.content() 20 | 21 | with zipfile.ZipFile(buffer, 'w') as zip_file: 22 | zip_file.writestr('requirements.txt', 'discord.py') 23 | 24 | zip_file.writestr('main.py', "print('ok')") 25 | 26 | zip_file.writestr('squarecloud.app', config) 27 | 28 | buffer.seek(0) 29 | 30 | return buffer.getvalue() 31 | 32 | 33 | def _clear_listener_on_rerun(endpoint: Endpoint): 34 | def decorator(func): 35 | async def wrapper(self, app: Application | Client, *args, **kwargs): 36 | if app.get_listener(endpoint): 37 | app.remove_listener(endpoint) 38 | return await func(self, app=app) 39 | 40 | return wrapper 41 | 42 | return decorator 43 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | from random import choice 4 | from string import ascii_letters 5 | from typing import AsyncGenerator 6 | 7 | import pytest 8 | from dotenv import load_dotenv 9 | from rich.status import Status 10 | 11 | from squarecloud import Client, File, FileInfo, UploadData 12 | from squarecloud.app import Application 13 | from squarecloud.utils import ConfigFile 14 | from tests import create_zip 15 | 16 | 17 | @pytest.fixture(scope='session') 18 | def event_loop() -> None: 19 | """Overrides pytest default function scoped event loop""" 20 | policy = asyncio.get_event_loop_policy() 21 | loop = policy.new_event_loop() 22 | yield loop 23 | loop.close() 24 | 25 | 26 | @pytest.fixture(scope='session') 27 | def client() -> Client: 28 | load_dotenv() 29 | return Client(os.getenv('KEY')) 30 | 31 | 32 | @pytest.fixture(scope='session') 33 | async def app(client: Client) -> AsyncGenerator[Application, None]: 34 | config = ConfigFile( 35 | display_name='normal_test', 36 | main='main.py', 37 | memory=512, 38 | subdomain=''.join(choice(ascii_letters) for _ in range(8)), 39 | ) 40 | with Status('uploading test application...', spinner='point'): 41 | upload_data: UploadData = await client.upload_app( 42 | File(create_zip(config), filename='file.zip') 43 | ) 44 | yield await client.app(upload_data.id) 45 | await client.delete_app(upload_data.id) 46 | 47 | 48 | @pytest.fixture(scope='module') 49 | async def app_files(app: Application) -> list[FileInfo]: 50 | return await app.files_list(path='/') 51 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | from zipfile import ZipFile 2 | 3 | import pytest 4 | 5 | import squarecloud 6 | from squarecloud import BackupInfo 7 | from squarecloud.app import Application 8 | from squarecloud.http import Response 9 | from tests import GITHUB_ACCESS_TOKEN 10 | 11 | 12 | @pytest.mark.asyncio(scope='session') 13 | @pytest.mark.app 14 | class TestApp: 15 | async def test_magic_methods(self, app: Application): 16 | assert ( 17 | app.__repr__() 18 | == f'<{Application.__name__} tag={app.name} id={app.id}>' 19 | ) 20 | 21 | async def test_app_status(self, app: Application): 22 | assert isinstance(await app.status(), squarecloud.StatusData) 23 | 24 | async def test_app_logs(self, app: Application): 25 | assert isinstance(await app.logs(), squarecloud.LogsData) 26 | 27 | async def test_app_backup(self, app: Application): 28 | assert isinstance(await app.backup(), squarecloud.Backup) 29 | 30 | async def test_download_backup(self, app: Application): 31 | backup = await app.backup() 32 | zip_file = await backup.download() 33 | assert isinstance(zip_file, ZipFile) 34 | 35 | async def test_app_github_integration(self, app: Application): 36 | assert isinstance( 37 | await app.github_integration(GITHUB_ACCESS_TOKEN), str 38 | ) 39 | 40 | async def test_app_last_deploys(self, app: Application): 41 | assert isinstance(await app.last_deploys(), list) 42 | 43 | async def test_domain_analytics(self, app: Application): 44 | assert isinstance( 45 | await app.domain_analytics(), squarecloud.DomainAnalytics 46 | ) 47 | 48 | @pytest.mark.skip 49 | async def test_set_custom_domain(self, app: Application): 50 | assert isinstance(await app.set_custom_domain('test.com.br'), str) 51 | 52 | async def test_get_all_backups(self, app: Application): 53 | backups = await app.all_backups() 54 | assert isinstance(backups, list) 55 | assert isinstance(backups[0], BackupInfo) 56 | 57 | async def test_move_file(self, app: Application): 58 | response = await app.move_file('main.py', 'test.py') 59 | assert isinstance(response, Response) 60 | assert response.status == 'success' 61 | -------------------------------------------------------------------------------- /tests/test_app_data.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | import pytest 4 | 5 | from squarecloud.app import Application 6 | from squarecloud.data import Backup, LogsData, StatusData 7 | 8 | 9 | @pytest.mark.asyncio(scope='session') 10 | @pytest.mark.app_data 11 | class Tests: 12 | async def test_status(self, app: Application): 13 | cache = app.cache 14 | cache.clear() 15 | 16 | assert cache.status is None 17 | 18 | status = await app.status(update_cache=False) 19 | assert cache.status is None 20 | 21 | cache.update(status) 22 | assert isinstance(cache.status, StatusData) 23 | 24 | cache.clear() 25 | assert cache.status is None 26 | sleep(10) 27 | 28 | async def test_backup(self, app: Application): 29 | cache = app.cache 30 | 31 | assert cache.backup is None 32 | 33 | backup = await app.backup(update_cache=False) 34 | assert cache.backup is None 35 | 36 | cache.update(backup) 37 | assert isinstance(cache.backup, Backup) 38 | 39 | cache.clear() 40 | assert cache.backup is None 41 | 42 | async def test_logs(self, app: Application): 43 | cache = app.cache 44 | 45 | assert cache.logs is None 46 | 47 | logs = await app.logs(update_cache=False) 48 | assert cache.logs is None 49 | 50 | cache.update(logs) 51 | assert isinstance(cache.logs, LogsData) 52 | 53 | cache.clear() 54 | assert cache.logs is None 55 | -------------------------------------------------------------------------------- /tests/test_capture_listeners.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec 2 | 3 | import pytest 4 | 5 | if using_pydantic := find_spec("pydantic"): 6 | from pydantic import BaseModel 7 | 8 | from squarecloud import Endpoint, errors 9 | from squarecloud.app import Application 10 | from squarecloud.data import Backup, LogsData, StatusData 11 | from squarecloud.listeners import Listener 12 | 13 | 14 | def _clear_listener_on_rerun(endpoint: Endpoint): 15 | def decorator(func): 16 | async def wrapper(self, app: Application): 17 | if app.get_listener(endpoint): 18 | app.remove_listener(endpoint) 19 | return await func(self, app=app) 20 | 21 | return wrapper 22 | 23 | return decorator 24 | 25 | 26 | @pytest.mark.asyncio(scope="class") 27 | @pytest.mark.listeners 28 | @pytest.mark.capture_listener 29 | class TestGeneralUse: 30 | @_clear_listener_on_rerun(Endpoint.app_status()) 31 | async def test_capture_status(self, app: Application): 32 | @app.capture(Endpoint.app_status(), force_raise=True) 33 | async def capture_status(before, after): 34 | assert before is None 35 | assert isinstance(after, StatusData) 36 | 37 | await app.status() 38 | 39 | @_clear_listener_on_rerun(Endpoint.backup()) 40 | async def test_capture_backup(self, app: Application): 41 | @app.capture(Endpoint.backup(), force_raise=True) 42 | async def capture_backup(before, after): 43 | assert before is None 44 | assert isinstance(after, Backup) 45 | 46 | await app.backup() 47 | 48 | @_clear_listener_on_rerun(Endpoint.logs()) 49 | async def test_capture_logs(self, app: Application): 50 | @app.capture(Endpoint.logs(), force_raise=True) 51 | async def capture_logs(before, after): 52 | assert before is None 53 | assert isinstance(after, LogsData) 54 | 55 | await app.logs() 56 | 57 | @_clear_listener_on_rerun(Endpoint.app_status()) 58 | async def test_extra(self, app: Application): 59 | metadata: dict[str, int] = {"metadata": 69} 60 | 61 | @app.capture(Endpoint.app_status(), force_raise=True) 62 | async def capture_status(extra): 63 | assert isinstance(extra, dict) 64 | assert extra == metadata 65 | 66 | await app.status(extra=metadata) 67 | 68 | @_clear_listener_on_rerun(Endpoint.app_status()) 69 | async def test_extra_is_none(self, app: Application): 70 | @app.capture(Endpoint.app_status(), force_raise=True) 71 | async def capture_status(extra): 72 | assert extra is None 73 | 74 | await app.status() 75 | 76 | @_clear_listener_on_rerun(Endpoint.app_status()) 77 | async def test_manage_listeners(self, app: Application): 78 | listener: Listener 79 | 80 | def callback_one(): 81 | pass 82 | 83 | listener = app.include_listener( 84 | Listener( 85 | app=app, 86 | endpoint=Endpoint.app_status(), 87 | callback=callback_one, 88 | ) 89 | ) 90 | 91 | assert app.get_listener(Endpoint.app_status()).callback is callback_one 92 | assert ( 93 | app.get_listener(Endpoint.app_status()).endpoint 94 | == Endpoint.app_status() 95 | ) 96 | assert not app.get_listener(Endpoint.app_status()).callback_params 97 | assert listener.callback is callback_one 98 | assert listener.endpoint == Endpoint.app_status() 99 | 100 | def callback_two(): 101 | pass 102 | 103 | with pytest.raises(errors.InvalidListener): 104 | app.include_listener( 105 | Listener( 106 | app=app, 107 | endpoint=Endpoint.app_status(), 108 | callback=callback_two, 109 | ) 110 | ) 111 | 112 | app.remove_listener(Endpoint.app_status()) 113 | assert app.get_listener(Endpoint.app_status()) is None 114 | 115 | listener = app.include_listener( 116 | Listener( 117 | app=app, 118 | endpoint=Endpoint.app_status(), 119 | callback=callback_two, 120 | ) 121 | ) 122 | assert listener.callback is callback_two 123 | assert listener.endpoint == Endpoint.app_status() 124 | 125 | @pytest.mark.skipif("not using_pydantic", reason="pydantic not installed") 126 | @_clear_listener_on_rerun(endpoint=Endpoint.app_status()) 127 | async def test_pydantic_cast(self, app: Application): 128 | class Person(BaseModel): 129 | name: str 130 | age: int 131 | 132 | class Car(BaseModel): 133 | year: int 134 | 135 | @app.capture(Endpoint.app_status(), force_raise=True) 136 | async def capture_status(extra: Person | Car | dict): 137 | assert isinstance(extra, Car) or isinstance(extra, Person) 138 | return extra 139 | 140 | await app.status(extra={"name": "Jhon", "age": 18}) 141 | await app.status(extra={"year": 1969}) 142 | -------------------------------------------------------------------------------- /tests/test_files.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from time import sleep 3 | 4 | import pytest 5 | 6 | import squarecloud as square 7 | from squarecloud import Client 8 | from squarecloud.app import Application 9 | from squarecloud.data import FileInfo 10 | 11 | 12 | @pytest.mark.asyncio(scope='session') 13 | @pytest.mark.files 14 | class TestFileClient: 15 | async def test_files_type(self, app_files: list[FileInfo]): 16 | assert isinstance(app_files, list) 17 | for file in app_files: 18 | assert isinstance(file, FileInfo) 19 | 20 | assert isinstance(file.name, str) 21 | assert isinstance(file.size, int) 22 | assert isinstance(file.lastModified, int | float | None) 23 | 24 | assert file.type in ('file', 'directory') 25 | 26 | async def test_read_file( 27 | self, client: Client, app: Application, app_files: list[FileInfo] 28 | ): 29 | file_read = await client.read_app_file(app.id, app_files[0].path) 30 | assert isinstance(file_read, BytesIO) 31 | 32 | async def test_create_file(self, client: Client, app: Application): 33 | await client.create_app_file( 34 | app.id, file=square.File('tests/test.txt'), path='/test.txt' 35 | ) 36 | 37 | async def test_delete_file(self, client: Client, app: Application): 38 | sleep(3) 39 | await client.delete_app_file(app.id, path='/test.txt') 40 | 41 | 42 | @pytest.mark.asyncio(scope='session') 43 | @pytest.mark.files 44 | class TestsApplication: 45 | async def test_files_list( 46 | self, app: Application, app_files: list[FileInfo] 47 | ): 48 | TestsApplication.TEST_FILES = await app.files_list(path='/') 49 | 50 | assert isinstance(app_files, list) 51 | for file in app_files: 52 | assert isinstance(file, FileInfo) 53 | 54 | assert isinstance(file.name, str) 55 | assert isinstance(file.size, int) 56 | assert isinstance(file.lastModified, int | float | None) 57 | 58 | assert file.type in ('file', 'directory') 59 | 60 | async def test_read_file( 61 | self, app: Application, app_files: list[FileInfo] 62 | ): 63 | file_read = await app.read_file(app_files[0].path) 64 | assert isinstance(file_read, BytesIO) 65 | 66 | async def test_create_file(self, app: Application): 67 | await app.create_file( 68 | file=square.File('tests/test.txt'), path='test.txt' 69 | ) 70 | 71 | async def test_delete_file(self, app: Application): 72 | sleep(3) 73 | await app.delete_file(path='/test.txt') 74 | -------------------------------------------------------------------------------- /tests/test_request_listeners.py: -------------------------------------------------------------------------------- 1 | from importlib.util import find_spec 2 | from io import BytesIO 3 | 4 | import pytest 5 | 6 | if using_pydantic := bool(find_spec('pydantic')): 7 | from pydantic import BaseModel 8 | 9 | from squarecloud import Client, Endpoint, File 10 | from squarecloud.app import Application 11 | from squarecloud.data import ( 12 | Backup, 13 | BackupInfo, 14 | DeployData, 15 | DomainAnalytics, 16 | FileInfo, 17 | LogsData, 18 | ResumedStatus, 19 | StatusData, 20 | UserData, 21 | ) 22 | from squarecloud.http import Response 23 | 24 | from . import GITHUB_ACCESS_TOKEN 25 | 26 | 27 | def _clear_listener_on_rerun(endpoint: Endpoint): 28 | def decorator(func): 29 | async def wrapper(self, client: Client, app: Application): 30 | if client.get_listener(endpoint): 31 | client.remove_listener(endpoint) 32 | return await func(self, client, app) 33 | 34 | return wrapper 35 | 36 | return decorator 37 | 38 | 39 | @pytest.mark.asyncio(scope='session') 40 | @pytest.mark.listeners 41 | @pytest.mark.request_listener 42 | class TestRequestListeners: 43 | @_clear_listener_on_rerun(Endpoint.logs()) 44 | async def test_request_logs(self, client: Client, app: Application): 45 | endpoint: Endpoint = Endpoint.logs() 46 | expected_result: LogsData | None 47 | expected_response: LogsData | None = None 48 | 49 | @client.on_request(endpoint) 50 | async def test_listener(response: Response): 51 | nonlocal expected_response 52 | expected_response = response 53 | 54 | expected_result = await client.get_logs(app.id) 55 | assert isinstance(expected_result, LogsData) 56 | assert isinstance(expected_response, Response) 57 | 58 | @_clear_listener_on_rerun(Endpoint.app_status()) 59 | async def test_request_app_status(self, client: Client, app: Application): 60 | endpoint: Endpoint = Endpoint.app_status() 61 | expected_result: StatusData | None 62 | expected_response: StatusData | None = None 63 | 64 | @client.on_request(endpoint) 65 | async def test_listener(response: Response): 66 | nonlocal expected_response 67 | expected_response = response 68 | 69 | expected_result = await client.app_status(app.id) 70 | assert isinstance(expected_result, StatusData) 71 | assert isinstance(expected_response, Response) 72 | 73 | @_clear_listener_on_rerun(Endpoint.backup()) 74 | async def test_request_backup(self, client: Client, app: Application): 75 | endpoint: Endpoint = Endpoint.backup() 76 | expected_result: Backup | None 77 | expected_response: Backup | None = None 78 | 79 | @client.on_request(endpoint) 80 | async def test_listener(response: Response): 81 | nonlocal expected_response 82 | expected_response = response 83 | 84 | expected_result = await client.backup(app.id) 85 | assert isinstance(expected_result, Backup) 86 | assert isinstance(expected_response, Response) 87 | 88 | @_clear_listener_on_rerun(Endpoint.start()) 89 | async def test_request_start_app(self, client: Client, app: Application): 90 | endpoint: Endpoint = Endpoint.start() 91 | expected_result: Response | None 92 | expected_response: Response | None = None 93 | 94 | @client.on_request(endpoint) 95 | async def test_listener(response: Response): 96 | nonlocal expected_response 97 | expected_response = response 98 | 99 | expected_result = await client.start_app(app.id) 100 | assert isinstance(expected_result, Response) 101 | assert isinstance(expected_response, Response) 102 | 103 | @_clear_listener_on_rerun(Endpoint.stop()) 104 | async def test_request_stop_app(self, client: Client, app: Application): 105 | endpoint: Endpoint = Endpoint.stop() 106 | expected_result: Response | None 107 | expected_response: Response | None = None 108 | 109 | @client.on_request(endpoint) 110 | async def test_listener(response: Response): 111 | nonlocal expected_response 112 | expected_response = response 113 | 114 | expected_result = await client.stop_app(app.id) 115 | assert isinstance(expected_result, Response) 116 | assert isinstance(expected_response, Response) 117 | 118 | @_clear_listener_on_rerun(Endpoint.restart()) 119 | async def test_request_restart_app(self, client: Client, app: Application): 120 | endpoint: Endpoint = Endpoint.restart() 121 | expected_result: Response | None 122 | expected_response: Response | None = None 123 | 124 | @client.on_request(endpoint) 125 | async def test_listener(response: Response): 126 | nonlocal expected_response 127 | expected_response = response 128 | 129 | expected_result = await client.restart_app(app.id) 130 | assert isinstance(expected_result, Response) 131 | assert isinstance(expected_response, Response) 132 | 133 | @_clear_listener_on_rerun(Endpoint.files_list()) 134 | async def test_request_app_files_list( 135 | self, client: Client, app: Application 136 | ): 137 | endpoint: Endpoint = Endpoint.files_list() 138 | expected_result: list[FileInfo] | None 139 | expected_response: Response | None = None 140 | 141 | @client.on_request(endpoint) 142 | async def test_listener(response: Response): 143 | nonlocal expected_response 144 | expected_response = response 145 | 146 | expected_result = await client.app_files_list(app.id, path='/') 147 | assert isinstance(expected_result, list) 148 | assert isinstance(expected_result[0], FileInfo) 149 | assert isinstance(expected_response, Response) 150 | 151 | @_clear_listener_on_rerun(Endpoint.files_read()) 152 | async def test_request_read_file(self, client: Client, app: Application): 153 | endpoint: Endpoint = Endpoint.files_read() 154 | expected_result: BytesIO | None 155 | expected_response: Response | None = None 156 | 157 | @client.on_request(endpoint) 158 | async def test_listener(response: Response): 159 | nonlocal expected_response 160 | expected_response = response 161 | 162 | expected_result = await client.read_app_file(app.id, '/main.py') 163 | assert isinstance(expected_result, BytesIO) 164 | assert isinstance(expected_response, Response) 165 | 166 | @_clear_listener_on_rerun(Endpoint.files_create()) 167 | async def test_request_create_file(self, client: Client, app: Application): 168 | endpoint: Endpoint = Endpoint.files_create() 169 | expected_result: Response | None 170 | expected_response: Response | None = None 171 | 172 | @client.on_request(endpoint) 173 | async def test_listener(response: Response): 174 | nonlocal expected_response 175 | expected_response = response 176 | 177 | expected_result = await client.create_app_file( 178 | app.id, File('tests/test.txt'), '/test.txt' 179 | ) 180 | assert isinstance(expected_result, Response) 181 | assert isinstance(expected_response, Response) 182 | 183 | @_clear_listener_on_rerun(Endpoint.files_delete()) 184 | async def test_request_delete_file(self, client: Client, app: Application): 185 | endpoint: Endpoint = Endpoint.files_delete() 186 | expected_result: Response | None 187 | expected_response: Response | None = None 188 | 189 | @client.on_request(endpoint) 190 | async def test_listener(response: Response): 191 | nonlocal expected_response 192 | expected_response = response 193 | 194 | expected_result = await client.delete_app_file(app.id, '/test.txt') 195 | assert isinstance(expected_result, Response) 196 | assert isinstance(expected_response, Response) 197 | 198 | @_clear_listener_on_rerun(Endpoint.commit()) 199 | async def test_request_commit(self, client: Client, app: Application): 200 | endpoint: Endpoint = Endpoint.commit() 201 | expected_result: Response | None 202 | expected_response: Response | None = None 203 | 204 | @client.on_request(endpoint) 205 | async def test_listener(response: Response): 206 | nonlocal expected_response 207 | expected_response = response 208 | 209 | expected_result = await client.commit(app.id, File('tests/test.txt')) 210 | assert isinstance(expected_result, Response) 211 | assert isinstance(expected_response, Response) 212 | 213 | @_clear_listener_on_rerun(Endpoint.user()) 214 | async def test_request_user(self, client: Client, app: Application): 215 | endpoint: Endpoint = Endpoint.user() 216 | expected_result: UserData | None 217 | expected_response: Response | None = None 218 | 219 | @client.on_request(endpoint) 220 | async def test_listener(response: Response): 221 | nonlocal expected_response 222 | expected_response = response 223 | 224 | expected_result = await client.user() 225 | assert isinstance(expected_result, UserData) 226 | assert isinstance(expected_response, Response) 227 | 228 | @_clear_listener_on_rerun(Endpoint.last_deploys()) 229 | async def test_last_deploys(self, client: Client, app: Application): 230 | endpoint: Endpoint = Endpoint.last_deploys() 231 | expected_result: list[list[DeployData]] | None 232 | expected_response: Response | None = None 233 | 234 | @client.on_request(endpoint) 235 | async def test_listener(response: Response): 236 | nonlocal expected_response 237 | expected_response = response 238 | 239 | expected_result = await client.last_deploys(app.id) 240 | assert isinstance(expected_result, list) 241 | assert isinstance(expected_response, Response) 242 | 243 | @_clear_listener_on_rerun(Endpoint.github_integration()) 244 | async def test_github_integration(self, client: Client, app: Application): 245 | endpoint: Endpoint = Endpoint.github_integration() 246 | expected_result: str | None 247 | expected_response: Response | None = None 248 | 249 | @client.on_request(endpoint) 250 | async def test_listener(response: Response): 251 | nonlocal expected_response 252 | expected_response = response 253 | 254 | expected_result = await client.github_integration( 255 | app.id, 256 | GITHUB_ACCESS_TOKEN, 257 | ) 258 | assert isinstance(expected_result, str) 259 | assert isinstance(expected_response, Response) 260 | 261 | @pytest.mark.skipif( 262 | lambda app: not app.is_website, reason='application is not website' 263 | ) 264 | @_clear_listener_on_rerun(Endpoint.domain_analytics()) 265 | async def test_domain_analytics(self, client: Client, app: Application): 266 | endpoint: Endpoint = Endpoint.domain_analytics() 267 | expected_result: DomainAnalytics | None 268 | expected_response: Response | None = None 269 | 270 | @client.on_request(endpoint) 271 | async def test_listener(response: Response): 272 | nonlocal expected_response 273 | expected_response = response 274 | 275 | expected_result = await client.domain_analytics(app.id) 276 | assert isinstance(expected_result, DomainAnalytics) 277 | assert isinstance(expected_response, Response) 278 | 279 | @pytest.mark.skipif( 280 | lambda app: not app.is_website, reason='application is not website' 281 | ) 282 | @_clear_listener_on_rerun(Endpoint.custom_domain()) 283 | async def test_set_custom_domain(self, client: Client, app: Application): 284 | endpoint: Endpoint = Endpoint.github_integration() 285 | expected_result: str | None 286 | expected_response: Response | None = None 287 | 288 | @client.on_request(endpoint) 289 | async def test_listener(response: Response): 290 | nonlocal expected_response 291 | expected_response = response 292 | 293 | expected_result = await client.github_integration( 294 | app.id, 295 | GITHUB_ACCESS_TOKEN, 296 | ) 297 | assert isinstance(expected_result, str) 298 | assert isinstance(expected_response, Response) 299 | 300 | @_clear_listener_on_rerun(Endpoint.all_backups()) 301 | async def test_all_backups(self, client: Client, app: Application): 302 | endpoint: Endpoint = Endpoint.all_backups() 303 | expected_result: list[BackupInfo] | None 304 | expected_response: Response | None = None 305 | 306 | @client.on_request(endpoint) 307 | async def test_listener(response: Response): 308 | nonlocal expected_response 309 | expected_response = response 310 | 311 | expected_result = await client.all_app_backups(app.id) 312 | assert isinstance(expected_result, list) 313 | assert isinstance(expected_result[0], BackupInfo) 314 | assert isinstance(expected_response, Response) 315 | 316 | @_clear_listener_on_rerun(Endpoint.all_apps_status()) 317 | async def test_all_apps_status(self, client: Client, app: Application): 318 | endpoint: Endpoint = Endpoint.all_apps_status() 319 | expected_result: list[ResumedStatus] | None 320 | expected_response: Response | None = None 321 | 322 | @client.on_request(endpoint) 323 | async def test_listener(response: Response): 324 | nonlocal expected_response 325 | expected_response = response 326 | 327 | expected_result = await client.all_apps_status() 328 | assert isinstance(expected_result, list) 329 | assert isinstance(expected_result[0], ResumedStatus) 330 | assert isinstance(expected_response, Response) 331 | 332 | @_clear_listener_on_rerun(Endpoint.move_file()) 333 | async def test_move_app_file(self, client: Client, app: Application): 334 | endpoint: Endpoint = Endpoint.move_file() 335 | expected_result: Response | None 336 | expected_response: Response | None = None 337 | 338 | @client.on_request(endpoint) 339 | async def test_listener(response: Response): 340 | nonlocal expected_response 341 | expected_response = response 342 | 343 | expected_result = await client.move_app_file( 344 | app.id, 'test.txt', 'test_move.txt' 345 | ) 346 | assert isinstance(expected_result, Response) 347 | assert isinstance(expected_response, Response) 348 | 349 | # @_clear_listener_on_rerun(Endpoint.dns_records()) 350 | # @pytest.mark.skipif( 351 | # lambda app: not app.custom, 352 | # reason='app have not custom domain', 353 | # ) 354 | # async def test_dns_records(self, client: Client, app: Application): 355 | # endpoint: Endpoint = Endpoint.dns_records() 356 | # expected_result: list[DNSRecord] | None 357 | # expected_response: Response | None = None 358 | # 359 | # @client.on_request(endpoint) 360 | # async def test_listener(response: Response): 361 | # nonlocal expected_response 362 | # expected_response = response 363 | # 364 | # expected_result = await client.dns_records(app.id) 365 | # assert isinstance(expected_result, list) 366 | # assert isinstance(expected_result[0], DNSRecord) 367 | # assert isinstance(expected_response, Response) 368 | 369 | @_clear_listener_on_rerun(Endpoint.current_integration()) 370 | async def test_current(self, client: Client, app: Application): 371 | endpoint: Endpoint = Endpoint.current_integration() 372 | expected_result: str | None 373 | expected_response: Response | None = None 374 | 375 | @client.on_request(endpoint) 376 | async def test_listener(response: Response): 377 | nonlocal expected_response 378 | expected_response = response 379 | 380 | expected_result = await client.current_app_integration(app.id) 381 | assert isinstance(expected_result, str) 382 | assert isinstance(expected_response, Response) 383 | 384 | @pytest.mark.skipif('not using_pydantic', reason='pydantic not installed') 385 | @_clear_listener_on_rerun(endpoint=Endpoint.app_status()) 386 | async def test_pydantic_cast(self, client: Client, app: Application): 387 | class Person(BaseModel): 388 | name: str 389 | age: int 390 | 391 | class Car(BaseModel): 392 | year: int 393 | 394 | @client.on_request(Endpoint.app_status(), force_raise=True) 395 | async def capture_status(extra: Person | Car | dict): 396 | assert isinstance(extra, Car) or isinstance(extra, Person) 397 | return extra 398 | 399 | await client.app_status( 400 | app_id=app.id, extra={'name': 'Jhon', 'age': 18} 401 | ) 402 | await client.app_status(app_id=app.id, extra={'year': 1969}) 403 | -------------------------------------------------------------------------------- /tests/test_upload_app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from squarecloud import Client, File, errors 6 | from squarecloud.data import UploadData 7 | from squarecloud.utils import ConfigFile 8 | 9 | from . import create_zip 10 | 11 | 12 | @pytest.mark.asyncio(scope='session') 13 | @pytest.mark.upload 14 | class TestRequestListeners: 15 | async def test_normal_upload(self, client: Client): 16 | config = ConfigFile( 17 | display_name='normal_test', 18 | main='main.py', 19 | memory=256, 20 | ) 21 | await asyncio.sleep(10) 22 | upload_data: UploadData = await client.upload_app( 23 | File(create_zip(config), filename='file.zip') 24 | ) 25 | await client.delete_app(upload_data.id) 26 | 27 | async def test_invalid_main_upload(self, client: Client): 28 | config = ConfigFile( 29 | display_name='invalid_main', 30 | main='index.js', 31 | memory=256, 32 | ) 33 | with pytest.raises(errors.InvalidMain): 34 | upload_data: UploadData = await client.upload_app( 35 | File(create_zip(config), filename='file.zip') 36 | ) 37 | await client.delete_app(upload_data.id) 38 | 39 | async def test_missing_main_upload(self, client: Client): 40 | config = ConfigFile( 41 | display_name='missing_main', 42 | main='', 43 | memory=256, 44 | ) 45 | with pytest.raises(errors.MissingMainFile): 46 | upload_data: UploadData = await client.upload_app( 47 | File(create_zip(config), filename='file.zip') 48 | ) 49 | await client.delete_app(upload_data.id) 50 | 51 | async def test_few_memory_upload(self, client: Client): 52 | config = ConfigFile( 53 | display_name='few_memory_test', 54 | main='main.py', 55 | memory=9999, 56 | ) 57 | with pytest.raises(errors.FewMemory): 58 | upload_data: UploadData = await client.upload_app( 59 | File(create_zip(config), filename='file.zip') 60 | ) 61 | await client.delete_app(upload_data.id) 62 | 63 | async def test_invalid_display_name_upload(self, client: Client): 64 | config = ConfigFile( 65 | display_name='test_' * 200, 66 | main='main.py', 67 | memory=256, 68 | ) 69 | with pytest.raises(errors.InvalidDisplayName): 70 | upload_data: UploadData = await client.upload_app( 71 | File(create_zip(config), filename='file.zip') 72 | ) 73 | await client.delete_app(upload_data.id) 74 | 75 | async def test_missing_display_name_upload(self, client: Client): 76 | config = ConfigFile( 77 | display_name='', 78 | main='main.py', 79 | memory=256, 80 | ) 81 | with pytest.raises(errors.MissingDisplayName): 82 | upload_data: UploadData = await client.upload_app( 83 | File(create_zip(config), filename='file.zip') 84 | ) 85 | await client.delete_app(upload_data.id) 86 | 87 | async def test_bad_memory_upload(self, client: Client): 88 | config = ConfigFile( 89 | display_name='memory_test', 90 | main='main.py', 91 | memory=256, 92 | ).content() 93 | with pytest.raises(errors.BadMemory): 94 | config = config.replace('256', '1') 95 | upload_data: UploadData = await client.upload_app( 96 | File(create_zip(config), filename='file.zip') 97 | ) 98 | await client.delete_app(upload_data.id) 99 | 100 | async def test_missing_memory_upload(self, client: Client): 101 | config = ConfigFile( 102 | display_name='memory_test', main='main.py', memory=256 103 | ).content() 104 | config = config.replace('256', '') 105 | with pytest.raises(errors.MissingMemory): 106 | upload_data: UploadData = await client.upload_app( 107 | File(create_zip(config), filename='file.zip') 108 | ) 109 | await client.delete_app(upload_data.id) 110 | 111 | async def test_invalid_version_upload(self, client: Client): 112 | config = ConfigFile( 113 | display_name='version_test', 114 | main='main.py', 115 | memory=256, 116 | ).content() 117 | config = config.replace('recommended', 'invalid_version') 118 | with pytest.raises(errors.InvalidVersion): 119 | upload_data: UploadData = await client.upload_app( 120 | File(create_zip(config), filename='file.zip') 121 | ) 122 | await client.delete_app(upload_data.id) 123 | 124 | async def test_missing_version_upload(self, client: Client): 125 | config = ConfigFile( 126 | display_name='version_test', 127 | main='main.py', 128 | memory=256, 129 | ).content() 130 | config = config.replace('recommended', '') 131 | with pytest.raises(errors.MissingVersion): 132 | upload_data: UploadData = await client.upload_app( 133 | File(create_zip(config), filename='file.zip') 134 | ) 135 | await client.delete_app(upload_data.id) 136 | --------------------------------------------------------------------------------