etherlyzer 0.5.2__tar.gz → 0.5.4__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 (27) hide show
  1. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/PKG-INFO +6 -6
  2. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/README.md +2 -2
  3. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/pyproject.toml +7 -7
  4. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/exporter.py +5 -5
  5. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/formatters.py +4 -4
  6. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/ieee/catalog.py +1 -1
  7. etherlyzer-0.5.2/src/etherlyzer/ieee/registry.py → etherlyzer-0.5.4/src/etherlyzer/ieee/ieee_registry.py +9 -9
  8. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/ieee/index.py +1 -1
  9. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/cli.py +3 -0
  10. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/identify.py +1 -1
  11. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/util/dirs.py +1 -1
  12. etherlyzer-0.5.4/src/etherlyzer/util/knobs.py +120 -0
  13. etherlyzer-0.5.2/src/etherlyzer/util/settings.py +0 -94
  14. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/LICENSE +0 -0
  15. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/__init__.py +0 -0
  16. {etherlyzer-0.5.2 → etherlyzer-0.5.4/src/etherlyzer}/etherlyzer.env +0 -0
  17. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/ieee/__init__.py +0 -0
  18. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/ieee/csv_parser.py +0 -0
  19. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/__init__.py +0 -0
  20. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/__init__.py +0 -0
  21. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/bulk.py +0 -0
  22. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/format.py +0 -0
  23. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/info.py +0 -0
  24. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/normalize.py +0 -0
  25. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/commands/update.py +0 -0
  26. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/terminal/utility.py +0 -0
  27. {etherlyzer-0.5.2 → etherlyzer-0.5.4}/src/etherlyzer/util/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etherlyzer
3
- Version: 0.5.2
3
+ Version: 0.5.4
4
4
  Summary: Fast offline lookup and classification of Ethernet-related identifiers.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.14
18
18
  Classifier: Topic :: System :: Networking
19
19
  Classifier: Topic :: Security
20
20
  Classifier: Typing :: Typed
21
- Requires-Dist: pandas (>=3.0.5,<4.0.0)
22
- Requires-Dist: platformdirs (>=4.10.0,<5.0.0)
23
- Requires-Dist: python-dotenv (>=1.2.2,<2.0.0)
21
+ Requires-Dist: pandas (>=3.0.6,<4.0.0)
22
+ Requires-Dist: platformdirs (>=4.12.2,<5.0.0)
23
+ Requires-Dist: python-dotenv (>=1.2.4,<2.0.0)
24
24
  Requires-Dist: requests (>=2.34.2,<3.0.0)
25
25
  Requires-Dist: tqdm (>=4.70.1,<5.0.0)
26
26
  Project-URL: Homepage, https://github.com/blue-hexagon/EtherLyzer
@@ -62,7 +62,7 @@ Typical use cases include:
62
62
 
63
63
  Etherlyzer automatically fetches the official IEEE registry listings once every 24 hours, ensuring that the local registry data remains up to date. IEEE states that its public listings are updated every 24 hours.
64
64
 
65
- The synchronization interval can be adjusted to your preference in the shipped `etherlyzer.env` file.
65
+ The synchronization interval can be adjusted to your preference in the shipped `src/etherlyzer/etherlyzer.env` file.
66
66
 
67
67
  ## Showcasing
68
68
 
@@ -226,7 +226,7 @@ etherlyzer format l..iIdOØ1!23-4oo.. --fix-typos --casing "upper" --block-size
226
226
 
227
227
  ## Configuration
228
228
 
229
- The included `etherlyzer.env` file provides sensible defaults and works out of the box but is configurable.
229
+ The included `src/etherlyzer/etherlyzer.env` file provides sensible defaults and works out of the box but is configurable.
230
230
 
231
231
 
232
232
  ```dotenv
@@ -32,7 +32,7 @@ Typical use cases include:
32
32
 
33
33
  Etherlyzer automatically fetches the official IEEE registry listings once every 24 hours, ensuring that the local registry data remains up to date. IEEE states that its public listings are updated every 24 hours.
34
34
 
35
- The synchronization interval can be adjusted to your preference in the shipped `etherlyzer.env` file.
35
+ The synchronization interval can be adjusted to your preference in the shipped `src/etherlyzer/etherlyzer.env` file.
36
36
 
37
37
  ## Showcasing
38
38
 
@@ -196,7 +196,7 @@ etherlyzer format l..iIdOØ1!23-4oo.. --fix-typos --casing "upper" --block-size
196
196
 
197
197
  ## Configuration
198
198
 
199
- The included `etherlyzer.env` file provides sensible defaults and works out of the box but is configurable.
199
+ The included `src/etherlyzer/etherlyzer.env` file provides sensible defaults and works out of the box but is configurable.
200
200
 
201
201
 
202
202
  ```dotenv
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "etherlyzer"
3
- version = "0.5.2"
3
+ version = "0.5.4"
4
4
  description = "Fast offline lookup and classification of Ethernet-related identifiers."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -38,9 +38,9 @@ classifiers = [
38
38
 
39
39
  dependencies = [
40
40
  "requests (>=2.34.2,<3.0.0)",
41
- "python-dotenv>=1.2.2,<2.0.0",
42
- "platformdirs (>=4.10.0,<5.0.0)",
43
- "pandas (>=3.0.5,<4.0.0)",
41
+ "python-dotenv>=1.2.4,<2.0.0",
42
+ "platformdirs (>=4.12.2,<5.0.0)",
43
+ "pandas (>=3.0.6,<4.0.0)",
44
44
  "tqdm (>=4.70.1,<5.0.0)",
45
45
  ]
46
46
 
@@ -58,15 +58,15 @@ packages = [
58
58
  { include = "etherlyzer", from = "src" }
59
59
  ]
60
60
  include = [
61
- { path = "etherlyzer.env", format = ["sdist", "wheel"] }
61
+ { path = "src/etherlyzer/etherlyzer.env", format = ["sdist", "wheel"] }
62
62
  ]
63
63
 
64
64
 
65
65
  [tool.poetry.group.dev.dependencies]
66
66
  pytest = "^9.1.1"
67
67
  pytest-cov = "^7.1.0"
68
- mypy = "^2.2.0"
69
- ruff = "^0.15.21"
68
+ mypy = "^2.4.0"
69
+ ruff = "^0.16.10"
70
70
 
71
71
 
72
72
  [tool.pytest.ini_options]
@@ -1,7 +1,7 @@
1
- from etherlyzer.util.settings import config
1
+ from etherlyzer.util.knobs import etherlyzer_knobs
2
2
  from etherlyzer.formatters import MACFormatter
3
3
 
4
- from etherlyzer.ieee.registry import IEEEEntry
4
+ from etherlyzer.ieee.ieee_registry import IEEEEntry
5
5
 
6
6
 
7
7
  class RegistryMatchExporter:
@@ -10,9 +10,9 @@ class RegistryMatchExporter:
10
10
  for entry in entries:
11
11
  try:
12
12
  print(
13
- f"{entry.registry}{config.field_separator}"
14
- f"{MACFormatter.format_default(entry.assignment)}{config.field_separator}"
15
- f"{entry.organization_name}{config.field_separator}"
13
+ f"{entry.registry}{etherlyzer_knobs.field_separator}"
14
+ f"{MACFormatter.format_default(entry.assignment)}{etherlyzer_knobs.field_separator}"
15
+ f"{entry.organization_name}{etherlyzer_knobs.field_separator}"
16
16
  f"{entry.organization_address}"
17
17
  f""
18
18
  )
@@ -1,7 +1,7 @@
1
1
  from enum import StrEnum
2
2
  from typing import Literal
3
3
 
4
- from etherlyzer.util.settings import config
4
+ from etherlyzer.util.knobs import etherlyzer_knobs
5
5
  from string import punctuation
6
6
 
7
7
 
@@ -43,11 +43,11 @@ class MACFormatter:
43
43
  @classmethod
44
44
  def format_default(cls, mac: str) -> str:
45
45
  mac = cls.normalize(mac)
46
- mac = config.mac_separator.join(
47
- mac[i: i + config.mac_block_size] for i in range(0, len(mac), config.mac_block_size)
46
+ mac = etherlyzer_knobs.mac_separator.join(
47
+ mac[i: i + etherlyzer_knobs.mac_block_size] for i in range(0, len(mac), etherlyzer_knobs.mac_block_size)
48
48
  )
49
49
 
50
- return getattr(mac, config.mac_case.value)()
50
+ return getattr(mac, etherlyzer_knobs.mac_case.value)()
51
51
 
52
52
  @classmethod
53
53
  def format_with_stuffed_hex(cls, mac: str, stuffed_hex: str) -> str:
@@ -1,4 +1,4 @@
1
- from etherlyzer.ieee.registry import RegCategory, IEEEEntry, EtherTypeEntry, IEEERegistry
1
+ from etherlyzer.ieee.ieee_registry import RegCategory, IEEEEntry, EtherTypeEntry, IEEERegistry
2
2
 
3
3
 
4
4
  class Catalog:
@@ -8,8 +8,8 @@ from requests import RequestException
8
8
  from tqdm import tqdm
9
9
 
10
10
  from etherlyzer.ieee.csv_parser import IEEERegistryReader
11
- from etherlyzer.util.dirs import IEEE_DATA_PLATDIR
12
- from etherlyzer.util.settings import config
11
+ from etherlyzer.util.dirs import DATA_PLATFORM_DIR
12
+ from etherlyzer.util.knobs import etherlyzer_knobs
13
13
 
14
14
 
15
15
  class RegCategory(StrEnum):
@@ -96,7 +96,7 @@ class IEEERegistry:
96
96
 
97
97
  @property
98
98
  def filepath(self) -> Path:
99
- return IEEE_DATA_PLATDIR / Path("".join([self.name, ".csv"]))
99
+ return DATA_PLATFORM_DIR / Path("".join([self.name, ".csv"]))
100
100
 
101
101
  @property
102
102
  def assignment_length(self) -> int:
@@ -123,11 +123,11 @@ class IEEERegistry:
123
123
  now = datetime.datetime.now(datetime.UTC)
124
124
  f_not_exists = not self.filepath.exists()
125
125
 
126
- if f_not_exists and config.show_sync_messages:
126
+ if f_not_exists and etherlyzer_knobs.show_sync_messages:
127
127
  print(f"Cache for {self.name} not found.")
128
- elif expires <= now and config.show_sync_messages:
128
+ elif expires <= now and etherlyzer_knobs.show_sync_messages:
129
129
  print(f"Cache for {self.name} has expired.")
130
- elif config.show_sync_messages:
130
+ elif etherlyzer_knobs.show_sync_messages:
131
131
  print(f"Cache for {self.name} expires at: {expires_humanized} UTC")
132
132
 
133
133
  return f_not_exists or expires <= now
@@ -138,7 +138,7 @@ class IEEERegistry:
138
138
 
139
139
  while attempts <= max_attempts:
140
140
  try:
141
- if config.show_sync_messages:
141
+ if etherlyzer_knobs.show_sync_messages:
142
142
  print(f"Saving Registry: {self.name}")
143
143
 
144
144
  res = requests.get(
@@ -158,7 +158,7 @@ class IEEERegistry:
158
158
  unit_scale=True,
159
159
  unit_divisor=1024,
160
160
  desc=f"Saving {self.name}",
161
- disable=not config.show_sync_messages,
161
+ disable=not etherlyzer_knobs.show_sync_messages,
162
162
  ) as progress:
163
163
  for chunk in res.iter_content(chunk_size=8192):
164
164
  if chunk:
@@ -168,7 +168,7 @@ class IEEERegistry:
168
168
  return None
169
169
 
170
170
  except RequestException as exc:
171
- if config.show_sync_messages:
171
+ if etherlyzer_knobs.show_sync_messages:
172
172
  print(
173
173
  f"Request timed out on attempt "
174
174
  f"{attempts} of {max_attempts}."
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from time import sleep
6
6
  from typing import TypeVar
7
7
 
8
- from etherlyzer.ieee.registry import RegCategory, IEEEEntry, EtherTypeEntry, IEEERegistry
8
+ from etherlyzer.ieee.ieee_registry import RegCategory, IEEEEntry, EtherTypeEntry, IEEERegistry
9
9
 
10
10
  HEX_DIGITS = frozenset("0123456789ABCDEF")
11
11
 
@@ -1,8 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ from pathlib import Path
4
5
 
5
6
  from etherlyzer.terminal.commands import info, format, normalize, identify, update, bulk
7
+ from etherlyzer.util.dirs import DATA_PLATFORM_DIR
8
+ from etherlyzer.util.knobs import etherlyzer_knobs
6
9
 
7
10
 
8
11
  def build_parser() -> argparse.ArgumentParser:
@@ -3,7 +3,7 @@ import re
3
3
  from etherlyzer.formatters import MACFormatter
4
4
  from etherlyzer.ieee.index import IEEEIndex
5
5
  from etherlyzer.ieee.catalog import Catalog
6
- from etherlyzer.ieee.registry import RegCategory, EtherTypeEntry
6
+ from etherlyzer.ieee.ieee_registry import RegCategory, EtherTypeEntry
7
7
 
8
8
 
9
9
  def run(args):
@@ -3,4 +3,4 @@ from pathlib import Path
3
3
  from platformdirs import user_cache_dir
4
4
 
5
5
  PROJECT_ROOT = Path(__file__).parent.parent.parent
6
- IEEE_DATA_PLATDIR = Path(user_cache_dir(appname="etherlyzer", appauthor="Manjana", version="1.0", ensure_exists=True))
6
+ DATA_PLATFORM_DIR = Path(user_cache_dir(appname="etherlyzer", appauthor="Manjana", version="1.0", ensure_exists=True))
@@ -0,0 +1,120 @@
1
+ import os
2
+ import shutil
3
+
4
+ from dataclasses import MISSING, dataclass, fields
5
+ from enum import Enum, StrEnum
6
+ from importlib.resources import files
7
+ from pathlib import Path
8
+
9
+ from dotenv import load_dotenv
10
+
11
+ from etherlyzer.util.dirs import DATA_PLATFORM_DIR
12
+
13
+
14
+ class MACCasing(StrEnum):
15
+ UPPER = "upper"
16
+ LOWER = "lower"
17
+
18
+
19
+ @dataclass(slots=True, frozen=True)
20
+ class EtherlyzerKnobs:
21
+ db_update_interval_hours: int = 24
22
+
23
+ field_separator: str = ";"
24
+ mac_separator: str = "."
25
+ mac_block_size: int = 4
26
+ mac_case: MACCasing = MACCasing.UPPER
27
+
28
+ show_sync_messages: bool = False
29
+
30
+
31
+ def _load_environment() -> None:
32
+ DATA_PLATFORM_DIR.mkdir(parents=True, exist_ok=True)
33
+
34
+ destination_env = DATA_PLATFORM_DIR / "etherlyzer.env"
35
+
36
+ if not destination_env.exists():
37
+ template_env = files("etherlyzer").joinpath("etherlyzer.env")
38
+
39
+ with template_env.open("rb") as src, destination_env.open("wb") as dst:
40
+ shutil.copyfileobj(src, dst)
41
+
42
+ load_dotenv(destination_env)
43
+
44
+
45
+ def _validate_knobs(etherlyzer_knobs: EtherlyzerKnobs) -> None:
46
+ if etherlyzer_knobs.db_update_interval_hours <= 0:
47
+ raise ValueError(
48
+ "DB_UPDATE_INTERVAL_HOURS must be greater than zero."
49
+ )
50
+
51
+ if len(etherlyzer_knobs.field_separator) != 1:
52
+ raise ValueError(
53
+ "FIELD_SEPARATOR must contain exactly one character."
54
+ )
55
+
56
+ if etherlyzer_knobs.mac_separator not in {"", ":", "-", "."}:
57
+ raise ValueError(
58
+ "MAC_SEPARATOR must be one of '', ':', '-', or '.'."
59
+ )
60
+
61
+ if etherlyzer_knobs.mac_block_size not in {2, 4, 12}:
62
+ raise ValueError(
63
+ "MAC_BLOCK_SIZE must be 2, 4, or 12."
64
+ )
65
+
66
+
67
+ def knobs_builder(cls: type[EtherlyzerKnobs]) -> EtherlyzerKnobs:
68
+ values = {}
69
+
70
+ for field in fields(cls):
71
+ env_name = field.name.upper()
72
+ value = os.getenv(env_name)
73
+
74
+ if value is None:
75
+ if field.default is not MISSING:
76
+ values[field.name] = field.default
77
+ continue
78
+
79
+ if field.default_factory is not MISSING:
80
+ values[field.name] = field.default_factory()
81
+ continue
82
+
83
+ raise ValueError(
84
+ f"Missing required environment variable {env_name}"
85
+ )
86
+
87
+ type_ = field.type
88
+
89
+ if type_ is bool:
90
+ values[field.name] = value.lower() in {
91
+ "1", "true", "yes", "on"
92
+ }
93
+
94
+ elif type_ is int:
95
+ values[field.name] = int(value)
96
+
97
+ elif type_ is float:
98
+ values[field.name] = float(value)
99
+
100
+ elif type_ is str:
101
+ values[field.name] = value
102
+
103
+ elif type_ is Path:
104
+ values[field.name] = Path(value)
105
+
106
+ elif issubclass(type_, Enum):
107
+ values[field.name] = type_(value)
108
+
109
+ else:
110
+ raise TypeError(
111
+ f"Unsupported config type: {type_}"
112
+ )
113
+
114
+ return cls(**values)
115
+
116
+
117
+ _load_environment()
118
+
119
+ etherlyzer_knobs = knobs_builder(EtherlyzerKnobs)
120
+ _validate_knobs(etherlyzer_knobs)
@@ -1,94 +0,0 @@
1
- import os
2
- from dataclasses import MISSING, dataclass, fields
3
- from enum import Enum, StrEnum
4
- from pathlib import Path
5
-
6
- from dotenv import load_dotenv
7
-
8
- load_dotenv("etherlyzer.env")
9
-
10
-
11
- class MACCase(StrEnum):
12
- UPPER = "upper"
13
- LOWER = "lower"
14
-
15
-
16
- @dataclass(slots=True, frozen=True)
17
- class Settings:
18
- db_update_interval_hours: int = 24
19
-
20
- field_separator: str = ";"
21
- mac_separator: str = "."
22
- mac_block_size: int = 4
23
- mac_case: MACCase = MACCase.UPPER
24
-
25
- show_sync_messages: bool = False
26
-
27
-
28
-
29
- def validate_config(conf: Settings) -> None:
30
- if conf.db_update_interval_hours <= 0:
31
- raise ValueError(
32
- "DB_UPDATE_INTERVAL_HOURS must be greater than zero."
33
- )
34
-
35
- if len(conf.field_separator) != 1:
36
- raise ValueError(
37
- "FIELD_SEPARATOR must contain exactly one character."
38
- )
39
-
40
- if conf.mac_separator not in {"", ":", "-", "."}:
41
- raise ValueError(
42
- "MAC_SEPARATOR must be one of '', ':', '-', or '.'."
43
- )
44
-
45
- if conf.mac_block_size not in {2, 4, 12}:
46
- raise ValueError(
47
- "MAC_BLOCK_SIZE must be 2, 4, or 12."
48
- )
49
-
50
-
51
- def load_config(cls: type[Settings]) -> Settings:
52
- values = {}
53
-
54
- for field in fields(cls):
55
- env_name = field.name.upper()
56
-
57
- value = os.getenv(env_name)
58
-
59
- if value is None:
60
- if field.default is not MISSING:
61
- values[field.name] = field.default
62
- elif field.default_factory is not MISSING:
63
- values[field.name] = field.default_factory()
64
- else:
65
- raise ValueError(
66
- f"Missing required environment variable {env_name}"
67
- )
68
-
69
- type_ = field.type
70
-
71
- if type_ is bool:
72
- values[field.name] = value.lower() in {"1", "true", "yes", "on"}
73
-
74
- elif type_ is int:
75
- values[field.name] = int(value)
76
-
77
- elif type_ is float:
78
- values[field.name] = float(value)
79
-
80
- elif type_ is str:
81
- values[field.name] = value
82
- elif type_ is Path:
83
- values[field.name] = Path(value)
84
-
85
- elif issubclass(type_, Enum):
86
- values[field.name] = type_(value)
87
-
88
- else:
89
- raise TypeError(f"Unsupported config type: {type_}")
90
- return cls(**values)
91
-
92
-
93
- config = load_config(Settings)
94
- validate_config(config)
File without changes