protonne.proton

  1from dataclasses import dataclass
  2from pathlib import Path
  3
  4from morbin import Morbin, Output
  5
  6
  7@dataclass
  8class Server:
  9    name: str
 10    country: str
 11    protocol: str
 12    load: str
 13    plan: str
 14    features: str | None
 15
 16
 17@dataclass(init=False)
 18class KillSwitch:
 19    active: bool
 20    on: bool
 21    permanent: bool
 22
 23    def __init__(self, status: str):
 24        if status == "Off":
 25            self.on = False
 26            self.active = False
 27            self.permanent = False
 28        elif status.startswith("On (Inactive,"):
 29            self.on = True
 30            self.active = False
 31            self.permanent = False
 32        elif status == "On":
 33            self.on = True
 34            self.active = True
 35            self.permanent = False
 36        elif status == "Permanent":
 37            self.on = True
 38            self.active = True
 39            self.permanent = True
 40
 41
 42@dataclass(init=False)
 43class Connection:
 44    IP: str
 45    killswitch: KillSwitch
 46    raw: str
 47    server: Server
 48    time: str
 49
 50    def __init__(self, status: str):
 51        self.raw = status
 52        lines = [line for line in status.splitlines() if ":" in line]
 53        connection = {
 54            line.split(":", 1)[0]: line.split(":", 1)[1].replace("\t", "").strip()
 55            for line in lines
 56        }
 57        self.IP = connection["IP"]
 58        self.server = Server(
 59            connection["Server"],
 60            connection["Country"],
 61            connection["Protocol"],
 62            connection["Server Load"],
 63            connection["Server Plan"],
 64            connection.get("Server Features"),
 65        )
 66        self.killswitch = KillSwitch(connection["Kill switch"])
 67        self.time = connection["Connection time"]
 68
 69    def __str__(self) -> str:
 70        return self.raw
 71
 72
 73class Proton(Morbin):
 74    @property
 75    def program(self) -> str:
 76        return "protonvpn-cli"
 77
 78    # Seat |=============================== Core ===============================|
 79
 80    def config(self, args: str = "") -> Output:
 81        return self.run(f"config {args}")
 82
 83    def connect(
 84        self,
 85        server: str = "",
 86        protocol: str = "",
 87        country_code: str = "",
 88        fastest: bool = False,
 89        random: bool = False,
 90        secure_core: bool = False,
 91        p2p: bool = False,
 92        tor: bool = False,
 93    ) -> Output:
 94        """
 95        #### :params:
 96
 97        `server`: Connect to this specific server (e.g. `CH#4`, `CH-US-1`, `HK5-Tor`).
 98
 99        `protocol`: Connect using this protocol (`tcp` or `udp`).
100
101        `country_code`: Connect to a server in this country (`SE`, `PT`, `BR`, `AR`, etc.).
102
103        `fastest`: Connect to the fastest server.
104
105        `random`: Connect to a random server.
106
107        `secure_core`: Connect to the fastest Secure-Core server.
108
109        `p2p`: Connect to the fastest P2P server.
110
111        `tor`: Connect to the fastest Tor server.
112
113        Supplying an argument for `server` will override other parameters except `protocol`.
114
115        If supplying one of the `bool` params (`fastest`, `random`, `secure_core`, `p2p`, `tor`), supply only one.
116        They will be checked in the order they appear in the function signature and the first that is `True` will be used.
117        """
118        args = ""
119        if server:
120            args = server
121        elif fastest:
122            args = "--fastest"
123        elif random:
124            args = "--random"
125        elif secure_core:
126            args = "--sc"
127        elif p2p:
128            args = "--p2p"
129        elif tor:
130            args = "--tor"
131        if country_code and (not server):
132            args += f" --cc {country_code}"
133        if protocol:
134            args += f" --protocol {protocol}"
135        return self.run(f"connect {args}")
136
137    def disconnect(self) -> Output:
138        return self.run("disconnect")
139
140    def killswitch(self, arg: str) -> Output:
141        """`arg` should be one of `on`, `off`, or `permanent`."""
142        return self.run(f"killswitch --{arg}")
143
144    def login(self, username: str) -> Output:
145        return self.run(f"login {username}")
146
147    def logout(self) -> Output:
148        return self.run("logout")
149
150    def netshield(self, args: str = "") -> Output:
151        return self.run(f"netshield {args}")
152
153    def reconnect(self) -> Output:
154        return self.run("reconnect")
155
156    def status(self) -> Output:
157        """Execute status command."""
158        return self.run("status")
159
160    # Seat |=========================== Convenience ===========================|
161
162    @property
163    def connected(self) -> bool:
164        """Returns whether this device is connected to Proton."""
165        with self.capturing_output():
166            status = self.status().stdout
167        return "Proton VPN Connection Status" in status
168
169    @property
170    def connection(self) -> Connection | None:
171        """If this device is connected, a `Connection` object will be returned.
172        If disconnected, `None` will be returned.
173
174        Accessing this property can be time consuming due to the protonvpn-cli backend.
175        Ideally store it in a local variable until you need to check for an updated connection status.
176        """
177        with self.capturing_output():
178            if self.connected:
179                return Connection(self.status().stdout)
180            return None
181
182    def clear_cache(self):
183        """Clears files from `home/.cache/protonvpn`.
184
185        This seems to help with the time taken to connect sometimes,
186        but if the permanent kill switch is engaged and you try to connect after clearing the cache, you will get an error.
187        """
188        path = Path.home() / ".cache" / "protonvpn"
189        if path.exists():
190            [path.unlink() for path in path.glob("*.*")]
191
192    def connect_fastest(self) -> Output:
193        """Connect to the fastest server."""
194        return self.connect(fastest=True)
195
196    def connect_random(self) -> Output:
197        """Connect to a random server."""
198        return self.connect(random=True)
199
200    def disable_killswitch(self) -> Output:
201        return self.killswitch("off")
202
203    def enable_killswitch(self) -> Output:
204        return self.killswitch("on")
205
206    def enable_permanent_killswitch(self) -> Output:
207        return self.killswitch("permanent")
@dataclass
class Server:
 8@dataclass
 9class Server:
10    name: str
11    country: str
12    protocol: str
13    load: str
14    plan: str
15    features: str | None
Server( name: str, country: str, protocol: str, load: str, plan: str, features: str | None)
@dataclass(init=False)
class KillSwitch:
18@dataclass(init=False)
19class KillSwitch:
20    active: bool
21    on: bool
22    permanent: bool
23
24    def __init__(self, status: str):
25        if status == "Off":
26            self.on = False
27            self.active = False
28            self.permanent = False
29        elif status.startswith("On (Inactive,"):
30            self.on = True
31            self.active = False
32            self.permanent = False
33        elif status == "On":
34            self.on = True
35            self.active = True
36            self.permanent = False
37        elif status == "Permanent":
38            self.on = True
39            self.active = True
40            self.permanent = True
KillSwitch(status: str)
24    def __init__(self, status: str):
25        if status == "Off":
26            self.on = False
27            self.active = False
28            self.permanent = False
29        elif status.startswith("On (Inactive,"):
30            self.on = True
31            self.active = False
32            self.permanent = False
33        elif status == "On":
34            self.on = True
35            self.active = True
36            self.permanent = False
37        elif status == "Permanent":
38            self.on = True
39            self.active = True
40            self.permanent = True
@dataclass(init=False)
class Connection:
43@dataclass(init=False)
44class Connection:
45    IP: str
46    killswitch: KillSwitch
47    raw: str
48    server: Server
49    time: str
50
51    def __init__(self, status: str):
52        self.raw = status
53        lines = [line for line in status.splitlines() if ":" in line]
54        connection = {
55            line.split(":", 1)[0]: line.split(":", 1)[1].replace("\t", "").strip()
56            for line in lines
57        }
58        self.IP = connection["IP"]
59        self.server = Server(
60            connection["Server"],
61            connection["Country"],
62            connection["Protocol"],
63            connection["Server Load"],
64            connection["Server Plan"],
65            connection.get("Server Features"),
66        )
67        self.killswitch = KillSwitch(connection["Kill switch"])
68        self.time = connection["Connection time"]
69
70    def __str__(self) -> str:
71        return self.raw
Connection(status: str)
51    def __init__(self, status: str):
52        self.raw = status
53        lines = [line for line in status.splitlines() if ":" in line]
54        connection = {
55            line.split(":", 1)[0]: line.split(":", 1)[1].replace("\t", "").strip()
56            for line in lines
57        }
58        self.IP = connection["IP"]
59        self.server = Server(
60            connection["Server"],
61            connection["Country"],
62            connection["Protocol"],
63            connection["Server Load"],
64            connection["Server Plan"],
65            connection.get("Server Features"),
66        )
67        self.killswitch = KillSwitch(connection["Kill switch"])
68        self.time = connection["Connection time"]
class Proton(morbin.morbin.Morbin):
 74class Proton(Morbin):
 75    @property
 76    def program(self) -> str:
 77        return "protonvpn-cli"
 78
 79    # Seat |=============================== Core ===============================|
 80
 81    def config(self, args: str = "") -> Output:
 82        return self.run(f"config {args}")
 83
 84    def connect(
 85        self,
 86        server: str = "",
 87        protocol: str = "",
 88        country_code: str = "",
 89        fastest: bool = False,
 90        random: bool = False,
 91        secure_core: bool = False,
 92        p2p: bool = False,
 93        tor: bool = False,
 94    ) -> Output:
 95        """
 96        #### :params:
 97
 98        `server`: Connect to this specific server (e.g. `CH#4`, `CH-US-1`, `HK5-Tor`).
 99
100        `protocol`: Connect using this protocol (`tcp` or `udp`).
101
102        `country_code`: Connect to a server in this country (`SE`, `PT`, `BR`, `AR`, etc.).
103
104        `fastest`: Connect to the fastest server.
105
106        `random`: Connect to a random server.
107
108        `secure_core`: Connect to the fastest Secure-Core server.
109
110        `p2p`: Connect to the fastest P2P server.
111
112        `tor`: Connect to the fastest Tor server.
113
114        Supplying an argument for `server` will override other parameters except `protocol`.
115
116        If supplying one of the `bool` params (`fastest`, `random`, `secure_core`, `p2p`, `tor`), supply only one.
117        They will be checked in the order they appear in the function signature and the first that is `True` will be used.
118        """
119        args = ""
120        if server:
121            args = server
122        elif fastest:
123            args = "--fastest"
124        elif random:
125            args = "--random"
126        elif secure_core:
127            args = "--sc"
128        elif p2p:
129            args = "--p2p"
130        elif tor:
131            args = "--tor"
132        if country_code and (not server):
133            args += f" --cc {country_code}"
134        if protocol:
135            args += f" --protocol {protocol}"
136        return self.run(f"connect {args}")
137
138    def disconnect(self) -> Output:
139        return self.run("disconnect")
140
141    def killswitch(self, arg: str) -> Output:
142        """`arg` should be one of `on`, `off`, or `permanent`."""
143        return self.run(f"killswitch --{arg}")
144
145    def login(self, username: str) -> Output:
146        return self.run(f"login {username}")
147
148    def logout(self) -> Output:
149        return self.run("logout")
150
151    def netshield(self, args: str = "") -> Output:
152        return self.run(f"netshield {args}")
153
154    def reconnect(self) -> Output:
155        return self.run("reconnect")
156
157    def status(self) -> Output:
158        """Execute status command."""
159        return self.run("status")
160
161    # Seat |=========================== Convenience ===========================|
162
163    @property
164    def connected(self) -> bool:
165        """Returns whether this device is connected to Proton."""
166        with self.capturing_output():
167            status = self.status().stdout
168        return "Proton VPN Connection Status" in status
169
170    @property
171    def connection(self) -> Connection | None:
172        """If this device is connected, a `Connection` object will be returned.
173        If disconnected, `None` will be returned.
174
175        Accessing this property can be time consuming due to the protonvpn-cli backend.
176        Ideally store it in a local variable until you need to check for an updated connection status.
177        """
178        with self.capturing_output():
179            if self.connected:
180                return Connection(self.status().stdout)
181            return None
182
183    def clear_cache(self):
184        """Clears files from `home/.cache/protonvpn`.
185
186        This seems to help with the time taken to connect sometimes,
187        but if the permanent kill switch is engaged and you try to connect after clearing the cache, you will get an error.
188        """
189        path = Path.home() / ".cache" / "protonvpn"
190        if path.exists():
191            [path.unlink() for path in path.glob("*.*")]
192
193    def connect_fastest(self) -> Output:
194        """Connect to the fastest server."""
195        return self.connect(fastest=True)
196
197    def connect_random(self) -> Output:
198        """Connect to a random server."""
199        return self.connect(random=True)
200
201    def disable_killswitch(self) -> Output:
202        return self.killswitch("off")
203
204    def enable_killswitch(self) -> Output:
205        return self.killswitch("on")
206
207    def enable_permanent_killswitch(self) -> Output:
208        return self.killswitch("permanent")

Base class for creating python bindings for cli programs.

At a minimum, any subclass must implement a program property that returns the name used to invoke the cli.

The run function can then be used to build bindings.

>>> class Pip(Morbin):
>>>     @property
>>>     def program(self)->str:
>>>         return 'pip'
>>>
>>>     def install(self, package:str, *args:str)->Output:
>>>         return self.run("install", package, *args)
>>>
>>>     def upgrade(self, package:str)->Output:
>>>         return self.install(package, "--upgrade")
>>>
>>>     def install_requirements(self)->Output:
>>>         return self.install("-r", "requirements.txt")
>>>
>>> pip = Pip()
>>> pip.upgrade("morbin")
program: str

The name used to invoke the program from the command line.

def config(self, args: str = '') -> morbin.morbin.Output:
81    def config(self, args: str = "") -> Output:
82        return self.run(f"config {args}")
def connect( self, server: str = '', protocol: str = '', country_code: str = '', fastest: bool = False, random: bool = False, secure_core: bool = False, p2p: bool = False, tor: bool = False) -> morbin.morbin.Output:
 84    def connect(
 85        self,
 86        server: str = "",
 87        protocol: str = "",
 88        country_code: str = "",
 89        fastest: bool = False,
 90        random: bool = False,
 91        secure_core: bool = False,
 92        p2p: bool = False,
 93        tor: bool = False,
 94    ) -> Output:
 95        """
 96        #### :params:
 97
 98        `server`: Connect to this specific server (e.g. `CH#4`, `CH-US-1`, `HK5-Tor`).
 99
100        `protocol`: Connect using this protocol (`tcp` or `udp`).
101
102        `country_code`: Connect to a server in this country (`SE`, `PT`, `BR`, `AR`, etc.).
103
104        `fastest`: Connect to the fastest server.
105
106        `random`: Connect to a random server.
107
108        `secure_core`: Connect to the fastest Secure-Core server.
109
110        `p2p`: Connect to the fastest P2P server.
111
112        `tor`: Connect to the fastest Tor server.
113
114        Supplying an argument for `server` will override other parameters except `protocol`.
115
116        If supplying one of the `bool` params (`fastest`, `random`, `secure_core`, `p2p`, `tor`), supply only one.
117        They will be checked in the order they appear in the function signature and the first that is `True` will be used.
118        """
119        args = ""
120        if server:
121            args = server
122        elif fastest:
123            args = "--fastest"
124        elif random:
125            args = "--random"
126        elif secure_core:
127            args = "--sc"
128        elif p2p:
129            args = "--p2p"
130        elif tor:
131            args = "--tor"
132        if country_code and (not server):
133            args += f" --cc {country_code}"
134        if protocol:
135            args += f" --protocol {protocol}"
136        return self.run(f"connect {args}")

:params:

server: Connect to this specific server (e.g. CH#4, CH-US-1, HK5-Tor).

protocol: Connect using this protocol (tcp or udp).

country_code: Connect to a server in this country (SE, PT, BR, AR, etc.).

fastest: Connect to the fastest server.

random: Connect to a random server.

secure_core: Connect to the fastest Secure-Core server.

p2p: Connect to the fastest P2P server.

tor: Connect to the fastest Tor server.

Supplying an argument for server will override other parameters except protocol.

If supplying one of the bool params (fastest, random, secure_core, p2p, tor), supply only one. They will be checked in the order they appear in the function signature and the first that is True will be used.

def disconnect(self) -> morbin.morbin.Output:
138    def disconnect(self) -> Output:
139        return self.run("disconnect")
def killswitch(self, arg: str) -> morbin.morbin.Output:
141    def killswitch(self, arg: str) -> Output:
142        """`arg` should be one of `on`, `off`, or `permanent`."""
143        return self.run(f"killswitch --{arg}")

arg should be one of on, off, or permanent.

def login(self, username: str) -> morbin.morbin.Output:
145    def login(self, username: str) -> Output:
146        return self.run(f"login {username}")
def logout(self) -> morbin.morbin.Output:
148    def logout(self) -> Output:
149        return self.run("logout")
def netshield(self, args: str = '') -> morbin.morbin.Output:
151    def netshield(self, args: str = "") -> Output:
152        return self.run(f"netshield {args}")
def reconnect(self) -> morbin.morbin.Output:
154    def reconnect(self) -> Output:
155        return self.run("reconnect")
def status(self) -> morbin.morbin.Output:
157    def status(self) -> Output:
158        """Execute status command."""
159        return self.run("status")

Execute status command.

connected: bool

Returns whether this device is connected to Proton.

connection: protonne.proton.Connection | None

If this device is connected, a Connection object will be returned. If disconnected, None will be returned.

Accessing this property can be time consuming due to the protonvpn-cli backend. Ideally store it in a local variable until you need to check for an updated connection status.

def clear_cache(self):
183    def clear_cache(self):
184        """Clears files from `home/.cache/protonvpn`.
185
186        This seems to help with the time taken to connect sometimes,
187        but if the permanent kill switch is engaged and you try to connect after clearing the cache, you will get an error.
188        """
189        path = Path.home() / ".cache" / "protonvpn"
190        if path.exists():
191            [path.unlink() for path in path.glob("*.*")]

Clears files from home/.cache/protonvpn.

This seems to help with the time taken to connect sometimes, but if the permanent kill switch is engaged and you try to connect after clearing the cache, you will get an error.

def connect_fastest(self) -> morbin.morbin.Output:
193    def connect_fastest(self) -> Output:
194        """Connect to the fastest server."""
195        return self.connect(fastest=True)

Connect to the fastest server.

def connect_random(self) -> morbin.morbin.Output:
197    def connect_random(self) -> Output:
198        """Connect to a random server."""
199        return self.connect(random=True)

Connect to a random server.

def disable_killswitch(self) -> morbin.morbin.Output:
201    def disable_killswitch(self) -> Output:
202        return self.killswitch("off")
def enable_killswitch(self) -> morbin.morbin.Output:
204    def enable_killswitch(self) -> Output:
205        return self.killswitch("on")
def enable_permanent_killswitch(self) -> morbin.morbin.Output:
207    def enable_permanent_killswitch(self) -> Output:
208        return self.killswitch("permanent")
Inherited Members
morbin.morbin.Morbin
Morbin
capture_output
shell
capturing_output
run