v2kit 0.4__tar.gz → 0.5__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.
Files changed (38) hide show
  1. {v2kit-0.4 → v2kit-0.5}/CHANGELOG.md +17 -1
  2. {v2kit-0.4 → v2kit-0.5}/PKG-INFO +21 -5
  3. {v2kit-0.4 → v2kit-0.5}/README.md +2 -2
  4. {v2kit-0.4 → v2kit-0.5}/SECURITY.md +2 -2
  5. {v2kit-0.4 → v2kit-0.5}/setup.py +2 -2
  6. {v2kit-0.4 → v2kit-0.5}/tests/test_http.py +15 -0
  7. {v2kit-0.4 → v2kit-0.5}/tests/test_shadowsocks.py +16 -1
  8. {v2kit-0.4 → v2kit-0.5}/tests/test_socks.py +15 -0
  9. {v2kit-0.4 → v2kit-0.5}/tests/test_trojan.py +14 -0
  10. {v2kit-0.4 → v2kit-0.5}/tests/test_vless.py +15 -0
  11. {v2kit-0.4 → v2kit-0.5}/tests/test_vmess.py +14 -0
  12. {v2kit-0.4 → v2kit-0.5}/v2kit/functions.py +10 -23
  13. {v2kit-0.4 → v2kit-0.5}/v2kit/models/base.py +19 -2
  14. {v2kit-0.4 → v2kit-0.5}/v2kit/models/http.py +5 -10
  15. {v2kit-0.4 → v2kit-0.5}/v2kit/models/shadowsocks.py +5 -7
  16. {v2kit-0.4 → v2kit-0.5}/v2kit/models/socks.py +5 -11
  17. {v2kit-0.4 → v2kit-0.5}/v2kit/models/trojan.py +5 -10
  18. {v2kit-0.4 → v2kit-0.5}/v2kit/models/vless.py +5 -10
  19. {v2kit-0.4 → v2kit-0.5}/v2kit/models/vmess.py +3 -3
  20. {v2kit-0.4 → v2kit-0.5}/v2kit/params.py +15 -3
  21. {v2kit-0.4 → v2kit-0.5}/v2kit/parsers.py +48 -80
  22. {v2kit-0.4 → v2kit-0.5}/v2kit/utils.py +3 -3
  23. {v2kit-0.4 → v2kit-0.5}/v2kit/validators.py +3 -3
  24. {v2kit-0.4 → v2kit-0.5}/v2kit.egg-info/PKG-INFO +21 -5
  25. {v2kit-0.4 → v2kit-0.5}/AUTHORS.md +0 -0
  26. {v2kit-0.4 → v2kit-0.5}/LICENSE +0 -0
  27. {v2kit-0.4 → v2kit-0.5}/MANIFEST.in +0 -0
  28. {v2kit-0.4 → v2kit-0.5}/requirements-dev.txt +0 -0
  29. {v2kit-0.4 → v2kit-0.5}/setup.cfg +0 -0
  30. {v2kit-0.4 → v2kit-0.5}/tests/test_errors.py +0 -0
  31. {v2kit-0.4 → v2kit-0.5}/tests/test_functions.py +0 -0
  32. {v2kit-0.4 → v2kit-0.5}/v2kit/__init__.py +0 -0
  33. {v2kit-0.4 → v2kit-0.5}/v2kit/__main__.py +0 -0
  34. {v2kit-0.4 → v2kit-0.5}/v2kit/errors.py +0 -0
  35. {v2kit-0.4 → v2kit-0.5}/v2kit/models/__init__.py +0 -0
  36. {v2kit-0.4 → v2kit-0.5}/v2kit.egg-info/SOURCES.txt +0 -0
  37. {v2kit-0.4 → v2kit-0.5}/v2kit.egg-info/dependency_links.txt +0 -0
  38. {v2kit-0.4 → v2kit-0.5}/v2kit.egg-info/top_level.txt +0 -0
@@ -5,6 +5,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
+ ## [0.5] - 2026-07-31
9
+ ### Added
10
+ - `encoded_label` property
11
+ - `uri_label` property
12
+ ### Changed
13
+ - `encode_subscription` function modified
14
+ - `parse` function modified
15
+ - `extra` property modified
16
+ - `update_extra` method modified
17
+ - `to_uri` method modified
18
+ - `to_dict` method modified
19
+ - `Protocol` enum updated
20
+ - Scheme bug fixed
21
+ - VMESS parser bug fixed
22
+ - Test system modified
8
23
  ## [0.4] - 2026-07-02
9
24
  ### Added
10
25
  - `HttpConfig` class
@@ -50,7 +65,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
50
65
  - `encode_subscription` function
51
66
  - `decode_subscription` function
52
67
 
53
- [Unreleased]: https://github.com/sepandhaghighi/v2kit/compare/v0.4...dev
68
+ [Unreleased]: https://github.com/sepandhaghighi/v2kit/compare/v0.5...dev
69
+ [0.5]: https://github.com/sepandhaghighi/v2kit/compare/v0.4...v0.5
54
70
  [0.4]: https://github.com/sepandhaghighi/v2kit/compare/v0.3...v0.4
55
71
  [0.3]: https://github.com/sepandhaghighi/v2kit/compare/v0.2...v0.3
56
72
  [0.2]: https://github.com/sepandhaghighi/v2kit/compare/v0.1...v0.2
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: v2kit
3
- Version: 0.4
3
+ Version: 0.5
4
4
  Summary: V2Kit: A Lightweight Toolkit for V2Ray Config Manipulation
5
5
  Home-page: https://github.com/sepandhaghighi/v2kit
6
- Download-URL: https://github.com/sepandhaghighi/v2kit/tarball/v0.4
6
+ Download-URL: https://github.com/sepandhaghighi/v2kit/tarball/v0.5
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
@@ -96,13 +96,13 @@ V2Kit is a lightweight and extensible Python toolkit for working with V2Ray prox
96
96
  ## Installation
97
97
 
98
98
  ### Source Code
99
- - Download [Version 0.4](https://github.com/sepandhaghighi/v2kit/archive/v0.4.zip) or [Latest Source](https://github.com/sepandhaghighi/v2kit/archive/dev.zip)
99
+ - Download [Version 0.5](https://github.com/sepandhaghighi/v2kit/archive/v0.5.zip) or [Latest Source](https://github.com/sepandhaghighi/v2kit/archive/dev.zip)
100
100
  - `pip install .`
101
101
 
102
102
  ### PyPI
103
103
 
104
104
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
105
- - `pip install v2kit==0.4`
105
+ - `pip install v2kit==0.5`
106
106
 
107
107
  ## Supported Protocols
108
108
 
@@ -211,6 +211,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
211
211
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
212
212
 
213
213
  ## [Unreleased]
214
+ ## [0.5] - 2026-07-31
215
+ ### Added
216
+ - `encoded_label` property
217
+ - `uri_label` property
218
+ ### Changed
219
+ - `encode_subscription` function modified
220
+ - `parse` function modified
221
+ - `extra` property modified
222
+ - `update_extra` method modified
223
+ - `to_uri` method modified
224
+ - `to_dict` method modified
225
+ - `Protocol` enum updated
226
+ - Scheme bug fixed
227
+ - VMESS parser bug fixed
228
+ - Test system modified
214
229
  ## [0.4] - 2026-07-02
215
230
  ### Added
216
231
  - `HttpConfig` class
@@ -256,7 +271,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
256
271
  - `encode_subscription` function
257
272
  - `decode_subscription` function
258
273
 
259
- [Unreleased]: https://github.com/sepandhaghighi/v2kit/compare/v0.4...dev
274
+ [Unreleased]: https://github.com/sepandhaghighi/v2kit/compare/v0.5...dev
275
+ [0.5]: https://github.com/sepandhaghighi/v2kit/compare/v0.4...v0.5
260
276
  [0.4]: https://github.com/sepandhaghighi/v2kit/compare/v0.3...v0.4
261
277
  [0.3]: https://github.com/sepandhaghighi/v2kit/compare/v0.2...v0.3
262
278
  [0.2]: https://github.com/sepandhaghighi/v2kit/compare/v0.1...v0.2
@@ -51,13 +51,13 @@ V2Kit is a lightweight and extensible Python toolkit for working with V2Ray prox
51
51
  ## Installation
52
52
 
53
53
  ### Source Code
54
- - Download [Version 0.4](https://github.com/sepandhaghighi/v2kit/archive/v0.4.zip) or [Latest Source](https://github.com/sepandhaghighi/v2kit/archive/dev.zip)
54
+ - Download [Version 0.5](https://github.com/sepandhaghighi/v2kit/archive/v0.5.zip) or [Latest Source](https://github.com/sepandhaghighi/v2kit/archive/dev.zip)
55
55
  - `pip install .`
56
56
 
57
57
  ### PyPI
58
58
 
59
59
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
60
- - `pip install v2kit==0.4`
60
+ - `pip install v2kit==0.5`
61
61
 
62
62
  ## Supported Protocols
63
63
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------------- | ------------------ |
7
- | 0.4 | :white_check_mark: |
8
- | < 0.4 | :x: |
7
+ | 0.5 | :white_check_mark: |
8
+ | < 0.5 | :x: |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
@@ -23,7 +23,7 @@ def read_description() -> str:
23
23
  setup(
24
24
  name='v2kit',
25
25
  packages=['v2kit', 'v2kit.models'],
26
- version='0.4',
26
+ version='0.5',
27
27
  description='V2Kit: A Lightweight Toolkit for V2Ray Config Manipulation',
28
28
  long_description=read_description(),
29
29
  long_description_content_type='text/markdown',
@@ -31,7 +31,7 @@ setup(
31
31
  author='Sepand Haghighi',
32
32
  author_email='me@sepand.tech',
33
33
  url='https://github.com/sepandhaghighi/v2kit',
34
- download_url='https://github.com/sepandhaghighi/v2kit/tarball/v0.4',
34
+ download_url='https://github.com/sepandhaghighi/v2kit/tarball/v0.5',
35
35
  keywords='v2ray v2ray-config v2ray-tools vmess vless trojan shadowsocks proxy subscription network',
36
36
  project_urls={
37
37
  'Source': 'https://github.com/sepandhaghighi/v2kit'},
@@ -137,6 +137,21 @@ def test_to_uri_roundtrip():
137
137
  assert parsed == config
138
138
 
139
139
 
140
+ def test_encoded_label():
141
+ config = HttpConfig(
142
+ address="example.com",
143
+ port=1080,
144
+ username="user",
145
+ password="password",
146
+ label="test 1 2",
147
+ )
148
+
149
+ parsed = parse(config.to_uri())
150
+ assert parsed == config
151
+ assert config.label == "test 1 2"
152
+ assert config.encoded_label == "test%201%202"
153
+
154
+
140
155
  def test_equality():
141
156
  config1 = HttpConfig(
142
157
  address="example.com",
@@ -113,6 +113,21 @@ def test_to_uri_roundtrip():
113
113
  assert parsed == config
114
114
 
115
115
 
116
+ def test_encoded_label():
117
+ config = ShadowsocksConfig(
118
+ encryption="aes-256-gcm",
119
+ password="password",
120
+ address="example.com",
121
+ port=8388,
122
+ label="test 1 2",
123
+ )
124
+
125
+ parsed = parse(config.to_uri())
126
+ assert parsed == config
127
+ assert config.label == "test 1 2"
128
+ assert config.encoded_label == "test%201%202"
129
+
130
+
116
131
  def test_equality():
117
132
  config1 = ShadowsocksConfig(
118
133
  encryption="aes-256-gcm",
@@ -150,7 +165,7 @@ def test_repr():
150
165
 
151
166
  assert repr(config) == (
152
167
  "ShadowsocksConfig(protocol=<Protocol.SHADOWSOCKS: "
153
- "'ss'>, label=None)"
168
+ "'shadowsocks'>, label=None)"
154
169
  )
155
170
 
156
171
 
@@ -135,6 +135,21 @@ def test_to_uri_roundtrip():
135
135
  assert parsed == config
136
136
 
137
137
 
138
+ def test_encoded_label():
139
+ config = SocksConfig(
140
+ address="example.com",
141
+ port=1080,
142
+ username="user",
143
+ password="password",
144
+ label="test 1 2",
145
+ )
146
+
147
+ parsed = parse(config.to_uri())
148
+ assert parsed == config
149
+ assert config.label == "test 1 2"
150
+ assert config.encoded_label == "test%201%202"
151
+
152
+
138
153
  def test_equality():
139
154
  config1 = SocksConfig(
140
155
  address="example.com",
@@ -131,6 +131,20 @@ def test_to_uri_roundtrip():
131
131
  assert parsed == config
132
132
 
133
133
 
134
+ def test_encoded_label():
135
+ config = TrojanConfig(
136
+ password="password",
137
+ address="example.com",
138
+ port=443,
139
+ label="test 1 2",
140
+ )
141
+
142
+ parsed = parse(config.to_uri())
143
+ assert parsed == config
144
+ assert config.label == "test 1 2"
145
+ assert config.encoded_label == "test%201%202"
146
+
147
+
134
148
  def test_update_extra():
135
149
  config = TrojanConfig(
136
150
  password="password",
@@ -105,6 +105,21 @@ def test_to_uri_roundtrip():
105
105
  assert parsed == config
106
106
 
107
107
 
108
+ def test_encoded_label():
109
+ config = VLESSConfig(
110
+ uuid="1c4b4bca-e3ff-4ca8-a062-6f399ad3cf45",
111
+ address="example.com",
112
+ port=443,
113
+ label="test 1 2",
114
+ extra={"security": "tls"},
115
+ )
116
+
117
+ parsed = parse(config.to_uri())
118
+ assert parsed == config
119
+ assert config.label == "test 1 2"
120
+ assert config.encoded_label == "test%201%202"
121
+
122
+
108
123
  def test_equality():
109
124
  config1 = VLESSConfig(
110
125
  uuid="1c4b4bca-e3ff-4ca8-a062-6f399ad3cf45",
@@ -147,6 +147,20 @@ def test_to_uri_roundtrip():
147
147
  assert parsed == config
148
148
 
149
149
 
150
+ def test_encoded_label():
151
+ config = VMESSConfig(
152
+ uuid="1c4b4bca-e3ff-4ca8-a062-6f399ad3cf45",
153
+ address="example.com",
154
+ port=443,
155
+ label="test 1 2",
156
+ )
157
+
158
+ parsed = parse(config.to_uri())
159
+ assert parsed == config
160
+ assert config.label == "test 1 2"
161
+ assert config.encoded_label == "test%201%202"
162
+
163
+
150
164
  def test_update_methods():
151
165
  config = VMESSConfig(
152
166
  uuid="1c4b4bca-e3ff-4ca8-a062-6f399ad3cf45",
@@ -91,33 +91,20 @@ def encode_subscription(
91
91
  uri_list = []
92
92
 
93
93
  for item in entries:
94
- if isinstance(
95
- item,
96
- BaseConfig,
97
- ):
98
- uri_list.append(
99
- item.to_uri()
100
- )
101
-
102
- continue
103
-
104
- if isinstance(item, str):
94
+ if isinstance(item, BaseConfig):
95
+ uri = item.to_uri()
96
+ elif isinstance(item, str):
97
+ uri = item
105
98
  if validate:
106
- parse(item)
99
+ parse(uri)
100
+ else:
101
+ raise V2kitValidationError(INVALID_ITEMS_MESSAGE)
107
102
 
108
- uri_list.append(item)
103
+ uri_list.append(uri)
109
104
 
110
- continue
105
+ subscription = "\n".join(uri_list)
111
106
 
112
- raise V2kitValidationError(INVALID_ITEMS_MESSAGE)
113
-
114
- subscription = "\n".join(
115
- uri_list
116
- )
117
-
118
- return _encode_base64(
119
- subscription
120
- )
107
+ return _encode_base64(subscription)
121
108
 
122
109
 
123
110
  def decode_subscription(
@@ -2,6 +2,7 @@
2
2
  """Base config model."""
3
3
 
4
4
  from abc import ABC, abstractmethod
5
+ from urllib.parse import quote
5
6
  from typing import Dict, Optional
6
7
  from ..params import Protocol
7
8
  from ..validators import _validate_label, _validate_dict
@@ -30,6 +31,7 @@ class BaseConfig(ABC):
30
31
  """
31
32
  self._protocol = protocol
32
33
  self._label = None
34
+ self._encoded_label = None
33
35
  self._extra = {}
34
36
 
35
37
  self.update_extra(extra or {})
@@ -45,10 +47,24 @@ class BaseConfig(ABC):
45
47
  """Get the config label."""
46
48
  return self._label
47
49
 
50
+ @property
51
+ def encoded_label(self) -> Optional[str]:
52
+ """Get the config percent-encoded label."""
53
+ return self._encoded_label
54
+
48
55
  @property
49
56
  def extra(self) -> Dict[str, object]:
50
57
  """Get extra data."""
51
- return self._extra
58
+ return self._extra.copy()
59
+
60
+ @property
61
+ def uri_label(self) -> str:
62
+ """Get URI label."""
63
+ return (
64
+ f"#{self.encoded_label}"
65
+ if self.encoded_label
66
+ else ""
67
+ )
52
68
 
53
69
  def update_label(
54
70
  self,
@@ -62,6 +78,7 @@ class BaseConfig(ABC):
62
78
  _validate_label(label)
63
79
 
64
80
  self._label = label
81
+ self._encoded_label = quote(label) if label else None
65
82
 
66
83
  return self
67
84
 
@@ -79,7 +96,7 @@ class BaseConfig(ABC):
79
96
  "Extra",
80
97
  )
81
98
 
82
- self._extra = extra
99
+ self._extra = extra.copy()
83
100
 
84
101
  return self
85
102
 
@@ -5,7 +5,7 @@ from urllib.parse import urlencode
5
5
  from typing import Dict, Optional
6
6
 
7
7
  from .base import BaseConfig
8
- from ..params import Protocol
8
+ from ..params import Protocol, PROTOCOL_SCHEMES
9
9
  from ..validators import (
10
10
  _validate_address,
11
11
  _validate_port,
@@ -128,7 +128,7 @@ class HttpConfig(BaseConfig):
128
128
  def to_dict(self) -> dict:
129
129
  """Convert HTTP config to dictionary."""
130
130
  return {
131
- "protocol": "http",
131
+ "protocol": self.protocol.value,
132
132
  "address": self.address,
133
133
  "port": self.port,
134
134
  "username": self.username,
@@ -153,14 +153,9 @@ class HttpConfig(BaseConfig):
153
153
  f"?{urlencode(self.extra)}"
154
154
  if self.extra else ""
155
155
  )
156
-
157
- label = (
158
- f"#{self.label}"
159
- if self.label else ""
160
- )
161
-
156
+ scheme = PROTOCOL_SCHEMES[self.protocol]
162
157
  return (
163
- f"http://{auth}"
158
+ f"{scheme}://{auth}"
164
159
  f"{self.address}:{self.port}"
165
- f"{query}{label}"
160
+ f"{query}{self.uri_label}"
166
161
  )
@@ -3,7 +3,7 @@
3
3
 
4
4
  from typing import Dict, Optional
5
5
  from .base import BaseConfig
6
- from ..params import Protocol
6
+ from ..params import Protocol, PROTOCOL_SCHEMES
7
7
  from ..validators import (
8
8
  _validate_port,
9
9
  _validate_address,
@@ -140,7 +140,7 @@ class ShadowsocksConfig(BaseConfig):
140
140
  def to_dict(self) -> dict:
141
141
  """Convert Shadowsocks config to dictionary."""
142
142
  return {
143
- "protocol": "shadowsocks",
143
+ "protocol": self.protocol.value,
144
144
  "encryption": self.encryption,
145
145
  "password": self.password,
146
146
  "address": self.address,
@@ -158,11 +158,9 @@ class ShadowsocksConfig(BaseConfig):
158
158
  encoded = _encode_base64(
159
159
  userinfo
160
160
  )
161
-
162
- label = f"#{self.label}" if self.label else ""
163
-
161
+ scheme = PROTOCOL_SCHEMES[self.protocol]
164
162
  return (
165
- f"ss://{encoded}@"
163
+ f"{scheme}://{encoded}@"
166
164
  f"{self.address}:{self.port}"
167
- f"{label}"
165
+ f"{self.uri_label}"
168
166
  )
@@ -4,7 +4,7 @@
4
4
  from urllib.parse import urlencode
5
5
  from typing import Dict, Optional
6
6
  from .base import BaseConfig
7
- from ..params import Protocol
7
+ from ..params import Protocol, PROTOCOL_SCHEMES
8
8
  from ..validators import (
9
9
  _validate_port,
10
10
  _validate_address,
@@ -133,7 +133,7 @@ class SocksConfig(BaseConfig):
133
133
  def to_dict(self) -> dict:
134
134
  """Convert SOCKS config to dictionary."""
135
135
  return {
136
- "protocol": "socks",
136
+ "protocol": self.protocol.value,
137
137
  "address": self.address,
138
138
  "port": self.port,
139
139
  "username": self.username,
@@ -148,12 +148,6 @@ class SocksConfig(BaseConfig):
148
148
  f"?{urlencode(self.extra)}"
149
149
  if self.extra else ""
150
150
  )
151
-
152
- label = (
153
- f"#{self.label}"
154
- if self.label else ""
155
- )
156
-
157
151
  auth = ""
158
152
 
159
153
  if self.username:
@@ -163,10 +157,10 @@ class SocksConfig(BaseConfig):
163
157
  auth += f":{self.password}"
164
158
 
165
159
  auth += "@"
166
-
160
+ scheme = PROTOCOL_SCHEMES[self.protocol]
167
161
  return (
168
- f"socks://"
162
+ f"{scheme}://"
169
163
  f"{auth}"
170
164
  f"{self.address}:{self.port}"
171
- f"{query}{label}"
165
+ f"{query}{self.uri_label}"
172
166
  )
@@ -4,7 +4,7 @@
4
4
  from urllib.parse import urlencode
5
5
  from typing import Dict, Optional
6
6
  from .base import BaseConfig
7
- from ..params import Protocol
7
+ from ..params import Protocol, PROTOCOL_SCHEMES
8
8
  from ..validators import (
9
9
  _validate_port,
10
10
  _validate_address,
@@ -113,7 +113,7 @@ class TrojanConfig(BaseConfig):
113
113
  def to_dict(self) -> dict:
114
114
  """Convert Trojan config to dictionary."""
115
115
  return {
116
- "protocol": "trojan",
116
+ "protocol": self.protocol.value,
117
117
  "password": self.password,
118
118
  "address": self.address,
119
119
  "port": self.port,
@@ -127,14 +127,9 @@ class TrojanConfig(BaseConfig):
127
127
  f"?{urlencode(self.extra)}"
128
128
  if self.extra else ""
129
129
  )
130
-
131
- label = (
132
- f"#{self.label}"
133
- if self.label else ""
134
- )
135
-
130
+ scheme = PROTOCOL_SCHEMES[self.protocol]
136
131
  return (
137
- f"trojan://{self.password}@"
132
+ f"{scheme}://{self.password}@"
138
133
  f"{self.address}:{self.port}"
139
- f"{query}{label}"
134
+ f"{query}{self.uri_label}"
140
135
  )
@@ -4,7 +4,7 @@
4
4
  from urllib.parse import urlencode
5
5
  from typing import Dict, Optional
6
6
  from .base import BaseConfig
7
- from ..params import Protocol
7
+ from ..params import Protocol, PROTOCOL_SCHEMES
8
8
  from ..validators import (
9
9
  _validate_uuid,
10
10
  _validate_port,
@@ -114,7 +114,7 @@ class VLESSConfig(BaseConfig):
114
114
  def to_dict(self) -> dict:
115
115
  """Convert VLESS config to dictionary."""
116
116
  return {
117
- "protocol": "vless",
117
+ "protocol": self.protocol.value,
118
118
  "uuid": self.uuid,
119
119
  "address": self.address,
120
120
  "port": self.port,
@@ -128,14 +128,9 @@ class VLESSConfig(BaseConfig):
128
128
  f"?{urlencode(self.extra)}"
129
129
  if self.extra else ""
130
130
  )
131
-
132
- label = (
133
- f"#{self.label}"
134
- if self.label else ""
135
- )
136
-
131
+ scheme = PROTOCOL_SCHEMES[self.protocol]
137
132
  return (
138
- f"vless://{self.uuid}@"
133
+ f"{scheme}://{self.uuid}@"
139
134
  f"{self.address}:{self.port}"
140
- f"{query}{label}"
135
+ f"{query}{self.uri_label}"
141
136
  )
@@ -4,7 +4,7 @@
4
4
  import json
5
5
  from typing import Dict, Optional
6
6
  from .base import BaseConfig
7
- from ..params import Protocol
7
+ from ..params import Protocol, PROTOCOL_SCHEMES
8
8
  from ..validators import (
9
9
  _validate_uuid,
10
10
  _validate_port,
@@ -213,5 +213,5 @@ class VMESSConfig(BaseConfig):
213
213
  ensure_ascii=False,
214
214
  )
215
215
  )
216
-
217
- return f"vmess://{encoded}"
216
+ scheme = PROTOCOL_SCHEMES[self.protocol]
217
+ return f"{scheme}://{encoded}"
@@ -2,7 +2,7 @@
2
2
  """v2kit params."""
3
3
  from enum import Enum
4
4
 
5
- V2KIT_VERSION = "0.4"
5
+ V2KIT_VERSION = "0.5"
6
6
 
7
7
 
8
8
  class Protocol(str, Enum):
@@ -11,12 +11,24 @@ class Protocol(str, Enum):
11
11
  VMESS = "vmess"
12
12
  VLESS = "vless"
13
13
  TROJAN = "trojan"
14
- SHADOWSOCKS = "ss"
14
+ SHADOWSOCKS = "shadowsocks"
15
15
  SOCKS = "socks"
16
16
  HTTP = "http"
17
17
 
18
18
 
19
- SUPPORTED_PROTOCOLS = {item.value for item in Protocol}
19
+ PROTOCOL_SCHEMES = {
20
+ Protocol.VMESS: "vmess",
21
+ Protocol.VLESS: "vless",
22
+ Protocol.TROJAN: "trojan",
23
+ Protocol.SHADOWSOCKS: "ss",
24
+ Protocol.SOCKS: "socks",
25
+ Protocol.HTTP: "http",
26
+ }
27
+
28
+ SCHEME_TO_PROTOCOL = {value: key for key, value in PROTOCOL_SCHEMES.items()}
29
+
30
+
31
+ SUPPORTED_PROTOCOLS = set(PROTOCOL_SCHEMES.values())
20
32
 
21
33
  DEFAULT_ENCODING = "utf-8"
22
34
 
@@ -3,54 +3,15 @@
3
3
 
4
4
  from typing import Union
5
5
  import json
6
- from urllib.parse import urlparse, parse_qsl
6
+ from urllib.parse import urlparse, parse_qsl, unquote
7
7
  from .errors import V2kitParseError
8
- from .params import Protocol
8
+ from .params import Protocol, SCHEME_TO_PROTOCOL
9
9
  from .params import INVALID_URI_FORMAT_MESSAGE, UNSUPPORTED_PROTOCOL_MESSAGE
10
10
  from .validators import _validate_non_empty_string
11
11
  from .models import VMESSConfig, VLESSConfig, TrojanConfig, ShadowsocksConfig, SocksConfig, HttpConfig
12
12
  from .utils import _decode_base64
13
13
 
14
14
 
15
- def parse(uri: str) -> Union[VMESSConfig, VLESSConfig, TrojanConfig, ShadowsocksConfig, SocksConfig, HttpConfig]:
16
- """
17
- Parse V2Ray URI.
18
-
19
- :param uri: V2Ray URI.
20
- """
21
- _validate_non_empty_string(uri, "URI")
22
-
23
- if "://" not in uri:
24
- raise V2kitParseError(INVALID_URI_FORMAT_MESSAGE)
25
-
26
- parsed = urlparse(uri)
27
-
28
- try:
29
- protocol = Protocol(
30
- parsed.scheme
31
- )
32
-
33
- except Exception as exc:
34
- raise V2kitParseError(UNSUPPORTED_PROTOCOL_MESSAGE.format(protocol=parsed.scheme)) from exc
35
-
36
- if protocol == Protocol.VMESS:
37
- return _parse_vmess(uri)
38
-
39
- if protocol == Protocol.VLESS:
40
- return _parse_vless(uri)
41
-
42
- if protocol == Protocol.TROJAN:
43
- return _parse_trojan(uri)
44
-
45
- if protocol == Protocol.SHADOWSOCKS:
46
- return _parse_shadowsocks(uri)
47
-
48
- if protocol == Protocol.SOCKS:
49
- return _parse_socks(uri)
50
- if protocol == Protocol.HTTP:
51
- return _parse_http(uri)
52
-
53
-
54
15
  def _parse_vmess(uri: str) -> VMESSConfig:
55
16
  """
56
17
  Parse VMESS URI.
@@ -67,17 +28,11 @@ def _parse_vmess(uri: str) -> VMESSConfig:
67
28
  "tls",
68
29
  }
69
30
  try:
70
- _, encoded = uri.split(
71
- "://",
72
- 1,
73
- )
74
-
75
- decoded = _decode_base64(
76
- encoded
77
- )
78
-
31
+ _, encoded = uri.split("://", 1)
32
+ decoded = _decode_base64(encoded)
79
33
  data = json.loads(decoded)
80
-
34
+ port = int(data.get("port", 0))
35
+ alter_id = int(data.get("aid", 0))
81
36
  except Exception as exc:
82
37
  raise V2kitParseError(INVALID_URI_FORMAT_MESSAGE) from exc
83
38
 
@@ -89,21 +44,11 @@ def _parse_vmess(uri: str) -> VMESSConfig:
89
44
  return VMESSConfig(
90
45
  uuid=data.get("id", ""),
91
46
  address=data.get("add", ""),
92
- port=int(
93
- data.get("port", 0)
94
- ),
47
+ port=port,
95
48
  label=data.get("ps"),
96
- alter_id=int(
97
- data.get("aid", 0)
98
- ),
99
- network=data.get(
100
- "net",
101
- "tcp",
102
- ),
103
- tls=data.get(
104
- "tls",
105
- "",
106
- ),
49
+ alter_id=alter_id,
50
+ network=data.get("net", "tcp"),
51
+ tls=data.get("tls", ""),
107
52
  extra=extra,
108
53
  )
109
54
 
@@ -119,7 +64,7 @@ def _parse_vless(uri: str) -> VLESSConfig:
119
64
  uuid=parsed.username or "",
120
65
  address=parsed.hostname or "",
121
66
  port=parsed.port or 0,
122
- label=parsed.fragment or None,
67
+ label=unquote(parsed.fragment) or None,
123
68
  extra=dict(parse_qsl(parsed.query)),
124
69
  )
125
70
 
@@ -135,7 +80,7 @@ def _parse_trojan(uri: str) -> TrojanConfig:
135
80
  password=parsed.username or "",
136
81
  address=parsed.hostname or "",
137
82
  port=parsed.port or 0,
138
- label=parsed.fragment or None,
83
+ label=unquote(parsed.fragment) or None,
139
84
  extra=dict(parse_qsl(parsed.query)),
140
85
  )
141
86
 
@@ -148,16 +93,8 @@ def _parse_shadowsocks(uri: str) -> ShadowsocksConfig:
148
93
  """
149
94
  parsed = urlparse(uri)
150
95
  try:
151
- userinfo = _decode_base64(
152
- parsed.username
153
- )
154
-
155
- encryption, password = (
156
- userinfo.split(
157
- ":",
158
- 1,
159
- )
160
- )
96
+ userinfo = _decode_base64(parsed.username)
97
+ encryption, password = (userinfo.split(":", 1))
161
98
 
162
99
  except Exception as exc:
163
100
  raise V2kitParseError(INVALID_URI_FORMAT_MESSAGE) from exc
@@ -167,7 +104,7 @@ def _parse_shadowsocks(uri: str) -> ShadowsocksConfig:
167
104
  password=password,
168
105
  address=parsed.hostname or "",
169
106
  port=parsed.port or 0,
170
- label=parsed.fragment or None,
107
+ label=unquote(parsed.fragment) or None,
171
108
  extra=dict(parse_qsl(parsed.query)),
172
109
  )
173
110
 
@@ -185,7 +122,7 @@ def _parse_socks(uri: str) -> SocksConfig:
185
122
  port=parsed.port or 0,
186
123
  username=parsed.username or None,
187
124
  password=parsed.password or None,
188
- label=parsed.fragment or None,
125
+ label=unquote(parsed.fragment) or None,
189
126
  extra=dict(parse_qsl(parsed.query)),
190
127
  )
191
128
 
@@ -203,6 +140,37 @@ def _parse_http(uri: str) -> HttpConfig:
203
140
  port=parsed.port or 0,
204
141
  username=parsed.username or None,
205
142
  password=parsed.password or None,
206
- label=parsed.fragment or None,
143
+ label=unquote(parsed.fragment) or None,
207
144
  extra=dict(parse_qsl(parsed.query)),
208
145
  )
146
+
147
+
148
+ PARSERS = {
149
+ Protocol.VMESS: _parse_vmess,
150
+ Protocol.VLESS: _parse_vless,
151
+ Protocol.TROJAN: _parse_trojan,
152
+ Protocol.SHADOWSOCKS: _parse_shadowsocks,
153
+ Protocol.SOCKS: _parse_socks,
154
+ Protocol.HTTP: _parse_http,
155
+ }
156
+
157
+
158
+ def parse(uri: str) -> Union[VMESSConfig, VLESSConfig, TrojanConfig, ShadowsocksConfig, SocksConfig, HttpConfig]:
159
+ """
160
+ Parse V2Ray URI.
161
+
162
+ :param uri: V2Ray URI.
163
+ """
164
+ _validate_non_empty_string(uri, "URI")
165
+
166
+ if "://" not in uri:
167
+ raise V2kitParseError(INVALID_URI_FORMAT_MESSAGE)
168
+
169
+ parsed = urlparse(uri)
170
+
171
+ try:
172
+ protocol = SCHEME_TO_PROTOCOL[parsed.scheme]
173
+ except Exception as exc:
174
+ raise V2kitParseError(UNSUPPORTED_PROTOCOL_MESSAGE.format(protocol=parsed.scheme)) from exc
175
+
176
+ return PARSERS[protocol](uri)
@@ -4,7 +4,7 @@ import base64
4
4
  from .errors import V2kitParseError
5
5
  from .validators import _validate_uri
6
6
  from .params import DEFAULT_ENCODING
7
- from .params import Protocol
7
+ from .params import Protocol, SCHEME_TO_PROTOCOL
8
8
 
9
9
 
10
10
  def _encode_base64(data: str) -> str:
@@ -48,9 +48,9 @@ def _get_protocol(uri: str) -> Protocol:
48
48
  """
49
49
  _validate_uri(uri)
50
50
 
51
- protocol = uri.split("://", 1)[0]
51
+ scheme = uri.split("://", 1)[0]
52
52
 
53
- return Protocol(protocol)
53
+ return SCHEME_TO_PROTOCOL[scheme]
54
54
 
55
55
 
56
56
  def _is_protocol(uri: str, protocol: Protocol) -> bool:
@@ -6,7 +6,7 @@ import base64
6
6
  import uuid
7
7
  from urllib.parse import urlparse
8
8
  from .errors import V2kitValidationError, V2kitParseError
9
- from .params import Protocol, DEFAULT_ENCODING
9
+ from .params import Protocol, SCHEME_TO_PROTOCOL, DEFAULT_ENCODING
10
10
  from .params import INVALID_TYPE_MESSAGE, INVALID_ALTER_ID_MESSAGE, INVALID_EMPTY_STRING_MESSAGE
11
11
  from .params import INVALID_UUID_MESSAGE, INVALID_PORT_MESSAGE, INVALID_URI_FORMAT_MESSAGE
12
12
  from .params import UNSUPPORTED_PROTOCOL_MESSAGE
@@ -166,10 +166,10 @@ def _validate_uri(uri: str) -> None:
166
166
  raise V2kitParseError(INVALID_URI_FORMAT_MESSAGE)
167
167
  parsed = urlparse(uri)
168
168
  try:
169
- Protocol(parsed.scheme)
169
+ SCHEME_TO_PROTOCOL[parsed.scheme]
170
170
  except Exception as exc:
171
171
  raise V2kitParseError(UNSUPPORTED_PROTOCOL_MESSAGE.format(protocol=parsed.scheme)) from exc
172
- if parsed.scheme == Protocol.VMESS.value:
172
+ if SCHEME_TO_PROTOCOL[parsed.scheme] == Protocol.VMESS:
173
173
  try:
174
174
  _, encoded = uri.split("://", 1)
175
175
  encoded += "=" * (-len(encoded) % 4)
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: v2kit
3
- Version: 0.4
3
+ Version: 0.5
4
4
  Summary: V2Kit: A Lightweight Toolkit for V2Ray Config Manipulation
5
5
  Home-page: https://github.com/sepandhaghighi/v2kit
6
- Download-URL: https://github.com/sepandhaghighi/v2kit/tarball/v0.4
6
+ Download-URL: https://github.com/sepandhaghighi/v2kit/tarball/v0.5
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
@@ -96,13 +96,13 @@ V2Kit is a lightweight and extensible Python toolkit for working with V2Ray prox
96
96
  ## Installation
97
97
 
98
98
  ### Source Code
99
- - Download [Version 0.4](https://github.com/sepandhaghighi/v2kit/archive/v0.4.zip) or [Latest Source](https://github.com/sepandhaghighi/v2kit/archive/dev.zip)
99
+ - Download [Version 0.5](https://github.com/sepandhaghighi/v2kit/archive/v0.5.zip) or [Latest Source](https://github.com/sepandhaghighi/v2kit/archive/dev.zip)
100
100
  - `pip install .`
101
101
 
102
102
  ### PyPI
103
103
 
104
104
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
105
- - `pip install v2kit==0.4`
105
+ - `pip install v2kit==0.5`
106
106
 
107
107
  ## Supported Protocols
108
108
 
@@ -211,6 +211,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
211
211
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
212
212
 
213
213
  ## [Unreleased]
214
+ ## [0.5] - 2026-07-31
215
+ ### Added
216
+ - `encoded_label` property
217
+ - `uri_label` property
218
+ ### Changed
219
+ - `encode_subscription` function modified
220
+ - `parse` function modified
221
+ - `extra` property modified
222
+ - `update_extra` method modified
223
+ - `to_uri` method modified
224
+ - `to_dict` method modified
225
+ - `Protocol` enum updated
226
+ - Scheme bug fixed
227
+ - VMESS parser bug fixed
228
+ - Test system modified
214
229
  ## [0.4] - 2026-07-02
215
230
  ### Added
216
231
  - `HttpConfig` class
@@ -256,7 +271,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
256
271
  - `encode_subscription` function
257
272
  - `decode_subscription` function
258
273
 
259
- [Unreleased]: https://github.com/sepandhaghighi/v2kit/compare/v0.4...dev
274
+ [Unreleased]: https://github.com/sepandhaghighi/v2kit/compare/v0.5...dev
275
+ [0.5]: https://github.com/sepandhaghighi/v2kit/compare/v0.4...v0.5
260
276
  [0.4]: https://github.com/sepandhaghighi/v2kit/compare/v0.3...v0.4
261
277
  [0.3]: https://github.com/sepandhaghighi/v2kit/compare/v0.2...v0.3
262
278
  [0.2]: https://github.com/sepandhaghighi/v2kit/compare/v0.1...v0.2
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes