airvpn 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
airvpn-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Crunchyroll+
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
airvpn-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: airvpn
3
+ Version: 0.1.0
4
+ Summary: A Python wrapper for the AirVPN API
5
+ Author: chonker
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ch0nker/airvpn
8
+ Project-URL: Repository, https://github.com/ch0nker/airvpn
9
+ Project-URL: Changelog, https://github.com/ch0nker/airvpn/tree/main/docs/CHANGELOG.md
10
+ Project-URL: Documentation, https://github.com/ch0nker/airvpn/tree/main/docs/api/README.md
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Requires-Python: >=3.11
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: requests
18
+ Requires-Dist: python-wireguard
19
+ Dynamic: license-file
20
+
21
+ # Python AirVPN Wrapper
22
+
23
+ > **Note:** The docstrings and most of the documentation was generated with AI. If you spot any errors I haven't caught yet, please open a PR or an issue.
24
+
25
+ A Python wrapper for AirVPN's [API](https://airvpn.org/apisettings/).
26
+
27
+ ## Guide
28
+ - [Changelog](docs/CHANGELOG.md)
29
+ - [Contributing](docs/CONTRIBUTING.md)
30
+ - [Example](#example)
31
+ - [API Documentation](docs/api/README.md)
32
+ - [Test Documentation](docs/tests/README.md)
33
+
34
+ ## Installation
35
+ ```bash
36
+ pip install airvpn
37
+ ```
38
+
39
+ ## Example
40
+ ```py
41
+ from airvpn import AirVPN
42
+
43
+ api = AirVPN(API_KEY) # API_KEY is optional for public services like whatismyip
44
+
45
+ print(api.userinfo.login)
46
+ ```
airvpn-0.1.0/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Python AirVPN Wrapper
2
+
3
+ > **Note:** The docstrings and most of the documentation was generated with AI. If you spot any errors I haven't caught yet, please open a PR or an issue.
4
+
5
+ A Python wrapper for AirVPN's [API](https://airvpn.org/apisettings/).
6
+
7
+ ## Guide
8
+ - [Changelog](docs/CHANGELOG.md)
9
+ - [Contributing](docs/CONTRIBUTING.md)
10
+ - [Example](#example)
11
+ - [API Documentation](docs/api/README.md)
12
+ - [Test Documentation](docs/tests/README.md)
13
+
14
+ ## Installation
15
+ ```bash
16
+ pip install airvpn
17
+ ```
18
+
19
+ ## Example
20
+ ```py
21
+ from airvpn import AirVPN
22
+
23
+ api = AirVPN(API_KEY) # API_KEY is optional for public services like whatismyip
24
+
25
+ print(api.userinfo.login)
26
+ ```
@@ -0,0 +1,164 @@
1
+ from airvpn.network import AirSession
2
+ from airvpn.devices import Devices, Device
3
+ from airvpn.dns_lists import DnsLists
4
+ from airvpn.generator import Generator
5
+ from airvpn.status import Status, Server
6
+ from airvpn.userinfo import UserInfo
7
+ from airvpn.whatismyip import WhatIsMyIp
8
+
9
+ from airvpn.disconnect import disconnect
10
+ from airvpn.notification import send_notification
11
+
12
+ class AirVPN:
13
+ """Main entry point for interacting with the AirVPN API.
14
+
15
+ Provides access to all available services (devices, DNS lists, config
16
+ generator, network status, user info, and IP lookup) as lazily-created,
17
+ cached properties, plus convenience methods for one-off actions like
18
+ sending notifications and disconnecting sessions.
19
+
20
+ Attributes:
21
+ api_key: The API key used for authenticated requests, if provided.
22
+ session: The underlying AirSession used to make API requests.
23
+ devices: The devices service, created and cached on first access.
24
+ dns_lists: The DNS lists service, created and cached on first access.
25
+ generator: The config generator service, created and cached on
26
+ first access.
27
+ status: The network status service, created and cached on first
28
+ access.
29
+ userinfo: The user info service, created and cached on first access.
30
+ whatismyip: The IP lookup service, created and cached on first
31
+ access.
32
+ """
33
+ __service_classes__ = {
34
+ "devices": Devices,
35
+ "dns_lists": DnsLists,
36
+ "generator": Generator,
37
+ "status": Status,
38
+ "userinfo": UserInfo,
39
+ "whatismyip": WhatIsMyIp
40
+ }
41
+
42
+ def __init__(self, API_KEY: str = None):
43
+ self.api_key = API_KEY
44
+ self.session = AirSession(API_KEY)
45
+
46
+ self._devices = None
47
+ self._dns_lists = None
48
+ self._generator = None
49
+ self._status = None
50
+ self._userinfo = None
51
+ self._whatismyip = None
52
+
53
+ def get_service(self, service: str):
54
+ """Instantiate a named service class, enforcing its API key requirement.
55
+
56
+ Args:
57
+ service: The service name (e.g. "devices", "status"), matched
58
+ case-insensitively against the registered service classes.
59
+
60
+ Returns:
61
+ An instance of the requested service class.
62
+
63
+ Raises:
64
+ AssertionError: If the service name is invalid.
65
+ Exception: If the service requires an API key and none was provided.
66
+ """
67
+ service_class = AirVPN.__service_classes__.get(service.lower())
68
+ assert service_class, f"Invalid service name: {service}"
69
+
70
+ service = service_class(self.session)
71
+ if service_class.KEY_NEEDED and not self.api_key:
72
+ raise Exception(f"API key is required to use \"{service}\"")
73
+
74
+ assert service, "Failed to create service"
75
+
76
+ return service
77
+
78
+ @property
79
+ def devices(self) -> Devices:
80
+ """The devices service, created and cached on first access."""
81
+ if not self._devices:
82
+ self._devices = self.get_service("devices")
83
+ return self._devices
84
+
85
+ @property
86
+ def dns_lists(self) -> DnsLists:
87
+ """The DNS lists service, created and cached on first access."""
88
+ if not self._dns_lists:
89
+ self._dns_lists = self.get_service("dns_lists")
90
+ return self._dns_lists
91
+
92
+ @property
93
+ def generator(self) -> Generator:
94
+ """The config generator service, created and cached on first access."""
95
+ if not self._generator:
96
+ self._generator = self.get_service("generator")
97
+ return self._generator
98
+
99
+ @property
100
+ def status(self) -> Status:
101
+ """The network status service, created and cached on first access."""
102
+ if not self._status:
103
+ self._status = self.get_service("status")
104
+ return self._status
105
+
106
+ @property
107
+ def userinfo(self) -> UserInfo:
108
+ """The user info service, created and cached on first access."""
109
+ if not self._userinfo:
110
+ self._userinfo = self.get_service("userinfo")
111
+ return self._userinfo
112
+
113
+ @property
114
+ def whatismyip(self) -> WhatIsMyIp:
115
+ """The IP lookup service, created and cached on first access."""
116
+ if not self._whatismyip:
117
+ self._whatismyip = self.get_service("whatismyip")
118
+ return self._whatismyip
119
+
120
+ def send_notification(self, subject: str, body: str):
121
+ """Send a message to yourself.
122
+
123
+ Args:
124
+ subject: The notification's subject line.
125
+ body: The notification's message content.
126
+
127
+ Returns:
128
+ True if the notification was sent successfully, False otherwise.
129
+
130
+ Access type:
131
+ User-specific, API KEY required.
132
+ """
133
+ assert self.api_key, "API key is required"
134
+
135
+ return send_notification(self.session, subject, body)
136
+
137
+ def disconnect(self,
138
+ server: Server = None,
139
+ device: Device = None,
140
+ server_name: str = None,
141
+ device_id: str = None):
142
+ """Requests a disconnection. If none of the filter parameters is specified, disconnect all sessions of the user.
143
+
144
+ Args:
145
+ server: A Server object to disconnect from; its public_name is
146
+ used if server_name is not explicitly provided.
147
+ device: A Device object to disconnect; its id is used if
148
+ device_id is not explicitly provided.
149
+ server_name: Name of the server to disconnect from. Ignored if
150
+ server is provided.
151
+ device_id: ID of the device to disconnect. Ignored if device
152
+ is provided.
153
+
154
+ Returns:
155
+ The number of sessions that were disconnected.
156
+
157
+ Access type:
158
+ User-specific, API KEY required.
159
+ """
160
+ assert self.api_key, "API key is required"
161
+
162
+ disconnect(self.session,
163
+ server_name=server.name if server else server_name,
164
+ device_id=device.id if device else device_id)
@@ -0,0 +1,125 @@
1
+ from airvpn.network import AirSession, Status
2
+ from airvpn.devices.models import Device
3
+
4
+ from enum import StrEnum
5
+
6
+ class DeviceAction(StrEnum):
7
+ """Actions available for managing devices via the devices endpoint."""
8
+ LIST = "list"
9
+ ADD = "add"
10
+ DELETE = "delete"
11
+ MODIFY = "modify"
12
+ RENEW = "renew"
13
+
14
+ class Devices:
15
+ """
16
+ Manages registered devices/keys associated with the account.
17
+
18
+ Access type:
19
+ User-specific, API KEY required
20
+ """
21
+
22
+ KEY_NEEDED = True
23
+
24
+ def __init__(self, session: AirSession):
25
+ self.session = session
26
+
27
+ def action(self,
28
+ action: DeviceAction,
29
+ id: str | None = None,
30
+ name: str | None = None,
31
+ description: str | None = None):
32
+ """Send a raw devices action request.
33
+
34
+ Args:
35
+ action: The device action to perform.
36
+ id: The device's ID. Required for delete, renew, and modify.
37
+ name: The device's name. Used for add and modify.
38
+ description: The device's description. Used for add and modify.
39
+
40
+ Returns:
41
+ The parsed JSON response from the devices endpoint.
42
+
43
+ Raises:
44
+ AssertionError: If the response contains an error.
45
+
46
+ """
47
+
48
+ result = self.session.get("devices", params={
49
+ "action": action, "id": id, "name": name, "description": description
50
+ }).json()
51
+
52
+ error = result.get("error", None)
53
+ assert not error, error
54
+
55
+ return result
56
+
57
+ def list(self):
58
+ """List all devices registered to the account.
59
+
60
+ Returns:
61
+ A list of Device objects.
62
+ """
63
+
64
+ response = self.action(DeviceAction.LIST)
65
+
66
+ return [Device(**device) for device in response.get("devices", [])]
67
+
68
+ def add(self):
69
+ """Register a new device.
70
+
71
+ Returns:
72
+ The ID of the newly created device, or None if not returned.
73
+ """
74
+
75
+ response = self.action(DeviceAction.ADD)
76
+
77
+ return response.get("id", None)
78
+
79
+ def delete(self, id: str):
80
+ """Delete a device.
81
+
82
+ Args:
83
+ id: The ID of the device to delete.
84
+
85
+ Returns:
86
+ True if successful in deleting the device.
87
+ """
88
+
89
+ response = self.action(DeviceAction.DELETE, id=id)
90
+
91
+ return response.get("result", "error") == "ok"
92
+
93
+ def renew(self, id: str):
94
+ """Renew a device.
95
+
96
+ Args:
97
+ id: The ID of the device to renew.
98
+
99
+ Returns:
100
+ True if successful in renewing the device.
101
+ """
102
+
103
+ response = self.action(DeviceAction.RENEW, id=id)
104
+
105
+ return response.get("result", "error") == "ok"
106
+
107
+ def modify(self, id: str, name: str | None = None, description: str | None = None):
108
+ """Modify a device's name and/or description.
109
+
110
+ Args:
111
+ id: The ID of the device to modify.
112
+ name: The new name for the device, if changing it.
113
+ description: The new description for the device, if changing it.
114
+
115
+ Returns:
116
+ True if successful in modifying the device.
117
+
118
+ Raises:
119
+ AssertionError: If neither name nor description is provided.
120
+ """
121
+ assert name is not None or description is not None, "You either need to modify name or description."
122
+
123
+ response = self.action(DeviceAction.MODIFY, id=id, name=name, description=description)
124
+
125
+ return response.get("result", "error") == "ok"
@@ -0,0 +1,70 @@
1
+ from typing import Unpack, TypedDict
2
+
3
+ class DeviceDict(TypedDict):
4
+ id: str
5
+ name: str
6
+ description: str
7
+ version: str
8
+ renew_first_unix: int
9
+ renew_first_date: str
10
+ renew_last_unix: int
11
+ renew_last_date: str
12
+ renew_counter: int
13
+ wireguard_public_key: str
14
+ wireguard_ipv4: str
15
+ wireguard_ipv6: str
16
+ vpn_last_from_unix: int
17
+ vpn_last_from_date: str
18
+ vpn_last_to_unix: int
19
+ vpn_last_to_date: str
20
+ vpn_attempt_unix: int
21
+ vpn_attempt_date: str
22
+ vpn_attempt_message: str
23
+ status: str
24
+
25
+ class Device:
26
+ """Represents a registered device associated with the account.
27
+
28
+ Attributes:
29
+ id: Unique identifier for the device.
30
+ name: Display name of the device.
31
+ description: Description of the device.
32
+ version: Version of the client/software associated with the device.
33
+ renew_first_unix: Unix timestamp of the device's first renewal.
34
+ renew_first_date: Human-readable date of the device's first renewal.
35
+ renew_last_unix: Unix timestamp of the device's most recent renewal.
36
+ renew_last_date: Human-readable date of the device's most recent renewal.
37
+ renew_counter: Number of times the device has been renewed.
38
+ wireguard_public_key: The device's WireGuard public key.
39
+ wireguard_ipv4: IPv4 address assigned to the device over WireGuard.
40
+ wireguard_ipv6: IPv6 address assigned to the device over WireGuard.
41
+ vpn_last_from_unix: Unix timestamp of the start of the device's last VPN session.
42
+ vpn_last_from_date: Human-readable start date of the device's last VPN session.
43
+ vpn_last_to_unix: Unix timestamp of the end of the device's last VPN session.
44
+ vpn_last_to_date: Human-readable end date of the device's last VPN session.
45
+ vpn_attempt_unix: Unix timestamp of the device's last connection attempt.
46
+ vpn_attempt_date: Human-readable date of the device's last connection attempt.
47
+ vpn_attempt_message: Message/result associated with the last connection attempt.
48
+ status: Current status of the device.
49
+ """
50
+ def __init__(self, **kwargs: Unpack[DeviceDict]):
51
+ self.id = kwargs.get("id")
52
+ self.name = kwargs.get("name")
53
+ self.description = kwargs.get("description")
54
+ self.version = kwargs.get("version")
55
+ self.renew_first_unix = kwargs.get("renew_first_unix")
56
+ self.renew_first_date = kwargs.get("renew_first_date")
57
+ self.renew_last_unix = kwargs.get("renew_last_unix")
58
+ self.renew_last_date = kwargs.get("renew_last_date")
59
+ self.renew_counter = kwargs.get("renew_counter")
60
+ self.wireguard_public_key = kwargs.get("wireguard_public_key")
61
+ self.wireguard_ipv4 = kwargs.get("wireguard_ipv4")
62
+ self.wireguard_ipv6 = kwargs.get("wireguard_ipv6")
63
+ self.vpn_last_from_unix = kwargs.get("vpn_last_from_unix")
64
+ self.vpn_last_from_date = kwargs.get("vpn_last_from_date")
65
+ self.vpn_last_to_unix = kwargs.get("vpn_last_to_unix")
66
+ self.vpn_last_to_date = kwargs.get("vpn_last_to_date")
67
+ self.vpn_attempt_unix = kwargs.get("vpn_attempt_unix")
68
+ self.vpn_attempt_date = kwargs.get("vpn_attempt_date")
69
+ self.vpn_attempt_message = kwargs.get("vpn_attempt_message")
70
+ self.status = kwargs.get("status")
@@ -0,0 +1,30 @@
1
+ from airvpn.network import AirSession
2
+
3
+
4
+ def disconnect(session: AirSession,
5
+ server_name: str = None,
6
+ device_id: str = None):
7
+ """Requests a disconnection. If none of the filter parameters is specified, disconnect all sessions of the user.
8
+
9
+ Args:
10
+ session: The active AirSession used to make the API request.
11
+ server_name: Name of the server to disconnect from. Ignored if
12
+ server is provided.
13
+ device_id: ID of the device to disconnect. Ignored if device
14
+ is provided.
15
+
16
+ Returns:
17
+ The number of sessions that were disconnected.
18
+
19
+ Access type:
20
+ User-specific, API KEY required.
21
+ """
22
+
23
+ response = session.get("disconnect", params={
24
+ "server": server_name,
25
+ "device": device_id
26
+ })
27
+
28
+ json = response.json()
29
+
30
+ return json.get("sessions_disconnected", 0)
@@ -0,0 +1,29 @@
1
+ """
2
+ I can't really understand the documentation for dns_lists.
3
+
4
+ If you can understand it or can get it to change at all please open an issue or create a PR with how you did it.
5
+ """
6
+
7
+ from airvpn.network import AirSession
8
+ from airvpn.dns_lists.models import Dns
9
+
10
+ class DnsLists:
11
+ """Fetches available DNS filtering lists.
12
+
13
+ Attributes:
14
+ lists: A dict mapping list keys to their corresponding Dns objects.
15
+
16
+ Access type:
17
+ Public, no API KEY required
18
+ """
19
+
20
+ KEY_NEEDED = False
21
+
22
+ def __init__(self, session: AirSession):
23
+ response = session.get("dns_lists")
24
+ json = response.json()
25
+
26
+ self.lists: dict[str, Dns] = {}
27
+
28
+ for key, dns in json.get("lists", {}).items():
29
+ self.lists[key] = Dns(**dns)
@@ -0,0 +1,26 @@
1
+ from typing import Unpack, TypedDict
2
+
3
+ class DnsDict(TypedDict, total=False):
4
+ name: str
5
+ description: str
6
+ home: str | None
7
+ last_update_unix: int
8
+ n_items: int
9
+
10
+ class Dns:
11
+ """Represents a DNS filtering list/profile.
12
+
13
+ Attributes:
14
+ name: The DNS list's name.
15
+ description: Description of the DNS list.
16
+ home: URL of the DNS list's home page/source, if available;
17
+ otherwise None.
18
+ last_update_unix: Unix timestamp of when the DNS list was last updated.
19
+ n_items: Number of entries in the DNS list.
20
+ """
21
+ def __init__(self, **kwargs: Unpack[DnsDict]):
22
+ self.name = kwargs.get("name")
23
+ self.description = kwargs.get("description")
24
+ self.home = kwargs.get("home")
25
+ self.last_update_unix = kwargs.get("last_update_unix")
26
+ self.n_items = kwargs.get("n_items")
@@ -0,0 +1,124 @@
1
+ # https://airvpn.org/api/generator/?protocols=wireguard_3_udp_1637&servers=xamidimura&device=Default
2
+ # https://airvpn.org/api/generator/?protocols=openvpn_3_udp_443%2Copenvpn_3_tcp_443%2Cwireguard_3_udp_1637%2Copenvpn_4_udp_443&servers=asellus&device=Default
3
+ # https://airvpn.org/api/generator/?protocols=openvpn_3_udp_443%2Copenvpn_3_tcp_443%2Cwireguard_3_udp_1637%2Copenvpn_4_udp_443&servers=grus%2Casellus&device=Default
4
+ # https://airvpn.org/api/generator/?protocols=openvpn_3_udp_443%2Copenvpn_3_tcp_443%2Cwireguard_3_udp_1637%2Copenvpn_4_udp_443&servers=america%2Casia%2Cearth%2Cbr%2Cca%2Cgrus%2Casellus%2Cvulpecula%2Cxamidimura&system=linux&device=Default
5
+
6
+ from typing import Unpack
7
+ from airvpn.network import AirSession
8
+ from airvpn.generator.models import SystemType, ProtocolType, VpnType, OptionsDict
9
+
10
+ class Generator:
11
+ """Generates VPN configuration files via the AirVPN config generator API.
12
+
13
+ Wraps the same endpoint used by https://airvpn.org/generator, allowing
14
+ programmatic creation of OpenVPN/WireGuard configs for one or more
15
+ servers, systems, and protocols.
16
+
17
+ Access type:
18
+ User-specific, API KEY required.
19
+ """
20
+
21
+ KEY_NEEDED = True
22
+
23
+ def __init__(self, session: AirSession):
24
+ self.session = session
25
+
26
+ def create_config(self,
27
+ server: str,
28
+ device: str,
29
+ system: SystemType = SystemType.WINDOWS,
30
+ vpn_type: VpnType = VpnType.WIREGUARD,
31
+ protocol_type: ProtocolType = ProtocolType.UDP,
32
+ port: int = 1637,
33
+ entry_ip: int = 3,
34
+ download: str = "auto",
35
+ files_binary: str = "",
36
+ files_prefix: str = "",
37
+ openvpn_directives: str = "",
38
+ openvpn_data_ciphers: str = "",
39
+ resolve: bool = False,
40
+ openvpn_allserver: bool = False,
41
+ proxy_mode: str = "none",
42
+ proxy_host: str = "127.0.0.1",
43
+ proxy_port: str = "8080",
44
+ proxy_login: str = "",
45
+ proxy_password: str = "",
46
+ proxy_auth: str = "none",
47
+ wireguard_mtu: int = 1320,
48
+ wireguard_persistent_keepalive: int = 15,
49
+ iplayer_entry: str = "ipv4",
50
+ iplayer_exit: str = "both",
51
+ **kwargs: Unpack[OptionsDict]
52
+ ):
53
+ """Generate a VPN configuration file for one or more servers.
54
+
55
+ Args:
56
+ server: Server name(s) to generate the config for. Multiple
57
+ servers, countries, continents, or "earth" can be combined
58
+ with commas (e.g. "america,asia,earth,br,ca").
59
+ device: The device profile name to associate with this config.
60
+ system: Target operating system for the generated config.
61
+ vpn_type: VPN protocol family (e.g. WireGuard, OpenVPN).
62
+ protocol_type: Transport protocol (UDP or TCP).
63
+ port: Port number to connect on.
64
+ entry_ip: IP version/entry point selector for the connection.
65
+ download: Download mode for the generated files ("auto" or
66
+ a specific format).
67
+ files_binary: Optional binary/executable to bundle with the config.
68
+ files_prefix: Optional filename prefix for generated files.
69
+ openvpn_directives: Additional custom directives to inject into
70
+ an OpenVPN config.
71
+ openvpn_data_ciphers: Custom data cipher list for OpenVPN.
72
+ resolve: Whether to resolve server hostnames instead of using
73
+ raw IPs in the config.
74
+ openvpn_allserver: Whether to include all servers in a single
75
+ OpenVPN config.
76
+ proxy_mode: Proxy mode to configure in the generated config
77
+ ("none" or a specific proxy type).
78
+ proxy_auth: Proxy authentication method/type, if proxy_mode
79
+ is enabled.
80
+ proxy_host: Proxy host address, if proxy_mode is enabled.
81
+ proxy_port: Proxy port, if proxy_mode is enabled.
82
+ proxy_login: Proxy authentication username, if required.
83
+ proxy_password: Proxy authentication password, if required.
84
+ wireguard_mtu: MTU value for WireGuard configs.
85
+ wireguard_persistent_keepalive: Persistent keepalive interval,
86
+ in seconds, for WireGuard configs.
87
+ iplayer_entry: IP layer to use for the entry connection
88
+ ("ipv4", "ipv6", or "both").
89
+ iplayer_exit: IP layer to use for the exit connection
90
+ ("ipv4", "ipv6", or "both").
91
+ **kwargs: Additional generator options not covered by the named
92
+ parameters above. These override any matching keys built
93
+ from the named parameters. Supported keys include:
94
+
95
+ openvpn_version: OpenVPN version to target.
96
+ openvpn_noembedkeys: Whether to omit embedding keys directly
97
+ in the OpenVPN config (reference external key files instead).
98
+ openvpn_allservers: Whether to include all servers in a single
99
+ OpenVPN config. Note: this is a separate key from the
100
+ openvpn_allserver named parameter above and does not
101
+ override it.
102
+
103
+ See OptionsDict for the full list of supported keys.
104
+
105
+ Returns:
106
+ The raw text response from the generator endpoint (typically
107
+ the generated config file contents).
108
+ """
109
+ options = locals()
110
+
111
+ options.pop("self")
112
+ options.pop("server")
113
+ options.pop("kwargs")
114
+
115
+ protocol = f"{vpn_type}_{entry_ip}_{protocol_type}_{port}"
116
+
117
+ options["protocols"] = protocol
118
+ options["servers"] = server
119
+
120
+ options = options | kwargs
121
+
122
+ response = self.session.get("generator", params=options)
123
+
124
+ return response.text