├── .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 |

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 |
--------------------------------------------------------------------------------