ptodnes 1.11.2.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.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: ptodnes
3
+ Version: 1.11.2.4
4
+ Summary:
5
+ License: MIT
6
+ Author: Ondrej Dohnal
7
+ Author-email: xdohna45@vutbr.cz
8
+ Requires-Python: >3.11,<=3.15
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: aiodns (>=3.2.0,<4.0.0)
15
+ Requires-Dist: aiofiles (>=25.1.0,<26.0.0)
16
+ Requires-Dist: aiohttp (>=3.11.13,<4.0.0)
17
+ Requires-Dist: aiopg (>=1.4.0,<2.0.0)
18
+ Requires-Dist: ptlibs (>=1.0.17,<2.0.0)
19
+ Requires-Dist: punycode (>=0.2.1,<0.3.0)
20
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ [![penterepTools](https://www.penterep.com/external/penterepToolsLogo.png)](https://www.penterep.com/)
24
+
25
+
26
+
27
+ ## ptodnes - OSINT Domain Name Enumeration System
28
+
29
+ ## Installation
30
+
31
+ 1. Download latest release whl package from [releases](https://github.com/Penterep/ptodnes/releases/latest) page.
32
+ 2. Install the package using pip/pipx.
33
+ ```bash
34
+ pipx install <path_to_downloaded_whl_file>
35
+ ```
36
+
37
+ Example:
38
+ ```bash
39
+ pipx install ~/Downloads/ptodnes-1.11.1-py3-none-any.whl
40
+ ```
41
+
42
+ ## Adding to PATH
43
+ If you're unable to invoke the script from your terminal, it's likely because it's not included in your PATH. You can resolve this issue by executing the following commands, depending on the shell you're using:
44
+
45
+ For Bash Users
46
+ ```bash
47
+ echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.bashrc
48
+ source ~/.bashrc
49
+ ```
50
+
51
+ For ZSH Users
52
+ ```bash
53
+ echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.zshrc
54
+ source ~/.zshrc
55
+ ```
56
+
57
+ ## Usage examples
58
+ ```
59
+ ptodnes -l
60
+ ptodnes -d example.com
61
+ ptodnes -d example.com example.net
62
+ ptodnes -d example.com -D VirusTotal CRTsh
63
+ ptodnes -d example.com -j -o example -t A AAAA
64
+ ptodnes -d example.com -D Wordlist -w /usr/share/wordlists/rockyou.txt
65
+ ```
66
+
67
+ ## Options
68
+ ```
69
+ -c --csv Output in CSV format
70
+ -C --config <config> Path to config file (default ~/ptodnes.toml)
71
+ -d --domain <domain ...> Domains to search for
72
+ -D --datasource <datasource ...> Datasources to browse
73
+ -e --exclude-unverified Exclude unverified records
74
+ -j --json Output in JSON format
75
+ -l --list List available datasources
76
+ -n --nonxdomain Filter results with no DNS data
77
+ -o --output <file_prefix> Save results to files (format specification required)
78
+ -p --ptjson Output in ptJSONlib format
79
+ -q --query Query domains against DNS servers
80
+ -r --retry <count> Number of attempts (default:5)
81
+ -t --type <type ...> Types of DNS records to search for
82
+ -T --timeout <timeout> Datasource connection timeout (in seconds, default:5)
83
+ -v --version Print version and exit
84
+ -V --verbose <1|2|3|4> Set verbosity level (1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG)
85
+ -w --wordlist <wordlist ...> Path to wordlist(s) for wordlist search.
86
+ -y --yaml Output in YAML format
87
+
88
+ ```
89
+
90
+ ## Configuration
91
+ Configuration is stored in TOML file. Default location is `~/ptodnes.toml`.
92
+
93
+ Example configuration:
94
+ ```toml
95
+ [VirusTotal]
96
+ api_keys = [
97
+ 'API_KEY_1',
98
+ 'API_KEY_2'
99
+ ]
100
+
101
+ [SecurityTrails]
102
+ api_keys = [
103
+ 'API_KEY_1',
104
+ 'API_KEY_2'
105
+ ]
106
+
107
+ [Wordlist]
108
+ wordlists = [
109
+ '/usr/share/wordlists/seclists/Discovery/DNS/namelist.txt'
110
+ ]
111
+
112
+ ...
113
+ [<Datasource>]
114
+ api_keys = [
115
+ '...'
116
+ ]
117
+ ```
118
+
119
+ ## Dependencies
120
+ ```
121
+ ptlibs
122
+ aiodns
123
+ aiohttp
124
+ aiopg
125
+ pyyaml
126
+ ```
127
+
128
+ ## License
129
+
130
+ Copyright (c) 2025 Penterep Security s.r.o.
131
+
132
+ ptodnes is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
133
+
134
+ ptodnes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
135
+
136
+ You should have received a copy of the GNU General Public License along with ptodnes. If not, see https://www.gnu.org/licenses/.
137
+
138
+ ## Warning
139
+
140
+ You are only allowed to run the tool against the websites which
141
+ you have been given permission to pentest. We do not accept any
142
+ responsibility for any damage/harm that this application causes to your
143
+ computer, or your network. Penterep is not responsible for any illegal
144
+ or malicious use of this code. Be Ethical!
145
+
@@ -0,0 +1,122 @@
1
+ [![penterepTools](https://www.penterep.com/external/penterepToolsLogo.png)](https://www.penterep.com/)
2
+
3
+
4
+
5
+ ## ptodnes - OSINT Domain Name Enumeration System
6
+
7
+ ## Installation
8
+
9
+ 1. Download latest release whl package from [releases](https://github.com/Penterep/ptodnes/releases/latest) page.
10
+ 2. Install the package using pip/pipx.
11
+ ```bash
12
+ pipx install <path_to_downloaded_whl_file>
13
+ ```
14
+
15
+ Example:
16
+ ```bash
17
+ pipx install ~/Downloads/ptodnes-1.11.1-py3-none-any.whl
18
+ ```
19
+
20
+ ## Adding to PATH
21
+ If you're unable to invoke the script from your terminal, it's likely because it's not included in your PATH. You can resolve this issue by executing the following commands, depending on the shell you're using:
22
+
23
+ For Bash Users
24
+ ```bash
25
+ echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.bashrc
26
+ source ~/.bashrc
27
+ ```
28
+
29
+ For ZSH Users
30
+ ```bash
31
+ echo "export PATH=\"`python3 -m site --user-base`/bin:\$PATH\"" >> ~/.zshrc
32
+ source ~/.zshrc
33
+ ```
34
+
35
+ ## Usage examples
36
+ ```
37
+ ptodnes -l
38
+ ptodnes -d example.com
39
+ ptodnes -d example.com example.net
40
+ ptodnes -d example.com -D VirusTotal CRTsh
41
+ ptodnes -d example.com -j -o example -t A AAAA
42
+ ptodnes -d example.com -D Wordlist -w /usr/share/wordlists/rockyou.txt
43
+ ```
44
+
45
+ ## Options
46
+ ```
47
+ -c --csv Output in CSV format
48
+ -C --config <config> Path to config file (default ~/ptodnes.toml)
49
+ -d --domain <domain ...> Domains to search for
50
+ -D --datasource <datasource ...> Datasources to browse
51
+ -e --exclude-unverified Exclude unverified records
52
+ -j --json Output in JSON format
53
+ -l --list List available datasources
54
+ -n --nonxdomain Filter results with no DNS data
55
+ -o --output <file_prefix> Save results to files (format specification required)
56
+ -p --ptjson Output in ptJSONlib format
57
+ -q --query Query domains against DNS servers
58
+ -r --retry <count> Number of attempts (default:5)
59
+ -t --type <type ...> Types of DNS records to search for
60
+ -T --timeout <timeout> Datasource connection timeout (in seconds, default:5)
61
+ -v --version Print version and exit
62
+ -V --verbose <1|2|3|4> Set verbosity level (1=ERROR, 2=WARNING, 3=INFO, 4=DEBUG)
63
+ -w --wordlist <wordlist ...> Path to wordlist(s) for wordlist search.
64
+ -y --yaml Output in YAML format
65
+
66
+ ```
67
+
68
+ ## Configuration
69
+ Configuration is stored in TOML file. Default location is `~/ptodnes.toml`.
70
+
71
+ Example configuration:
72
+ ```toml
73
+ [VirusTotal]
74
+ api_keys = [
75
+ 'API_KEY_1',
76
+ 'API_KEY_2'
77
+ ]
78
+
79
+ [SecurityTrails]
80
+ api_keys = [
81
+ 'API_KEY_1',
82
+ 'API_KEY_2'
83
+ ]
84
+
85
+ [Wordlist]
86
+ wordlists = [
87
+ '/usr/share/wordlists/seclists/Discovery/DNS/namelist.txt'
88
+ ]
89
+
90
+ ...
91
+ [<Datasource>]
92
+ api_keys = [
93
+ '...'
94
+ ]
95
+ ```
96
+
97
+ ## Dependencies
98
+ ```
99
+ ptlibs
100
+ aiodns
101
+ aiohttp
102
+ aiopg
103
+ pyyaml
104
+ ```
105
+
106
+ ## License
107
+
108
+ Copyright (c) 2025 Penterep Security s.r.o.
109
+
110
+ ptodnes is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
111
+
112
+ ptodnes is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
113
+
114
+ You should have received a copy of the GNU General Public License along with ptodnes. If not, see https://www.gnu.org/licenses/.
115
+
116
+ ## Warning
117
+
118
+ You are only allowed to run the tool against the websites which
119
+ you have been given permission to pentest. We do not accept any
120
+ responsibility for any damage/harm that this application causes to your
121
+ computer, or your network. Penterep is not responsible for any illegal
122
+ or malicious use of this code. Be Ethical!
File without changes
@@ -0,0 +1,92 @@
1
+ from typing import Generator, List
2
+
3
+ from ptodnes.DNS.record import DNSRecord
4
+ from ptodnes.datasources.datasource import DatasourceObject
5
+
6
+
7
+ class DNSRecordDict(dict[str, list[DNSRecord]]):
8
+ """
9
+ DNS Records dictionary
10
+ """
11
+ def append(self, item: DatasourceObject):
12
+ """
13
+ Add DNS record to dictionary. Checks if record exists.
14
+ :param item: DNS record
15
+ :return:
16
+ """
17
+ if item not in self.keys():
18
+ self[item.domain] = list(set(item.DNSData))
19
+ else:
20
+ for obj in item.DNSData:
21
+ if obj not in self[item.domain]:
22
+ self[item.domain].append(obj)
23
+ else:
24
+ for i in range(len(self[item.domain])):
25
+ if self[item.domain][i] == obj:
26
+ self[item.domain][i].source.update(obj.source)
27
+
28
+ def extend(self, items: list[DatasourceObject]):
29
+ """
30
+ Add DNS records to dictionary
31
+ :param items: DNS Records
32
+ :return:
33
+ """
34
+ for item in items:
35
+ self.append(item)
36
+ def filter(self, types: list):
37
+ """
38
+ Filter DNS records by type
39
+ :param types: types to filter
40
+ :return:
41
+ """
42
+ keys = []
43
+ filter_types = types.copy()
44
+ if 'ANY' in filter_types:
45
+ return
46
+ self.filterNX()
47
+ for key, value in self.items():
48
+ filtered = [x for x in filter((lambda i: i.type in filter_types), value)]
49
+ if not filtered:
50
+ keys.append(key)
51
+ self[key] = filtered
52
+ for key in keys:
53
+ del (self[key])
54
+
55
+ def filter_untrusted(self):
56
+ """
57
+ Filter records that have not been verified
58
+ :return:
59
+ """
60
+ keys = []
61
+ for key, value in self.items():
62
+ filtered = [x for x in filter((lambda i: i.verified), value)]
63
+ if not filtered:
64
+ keys.append(key)
65
+ self[key] = filtered
66
+ for key in keys:
67
+ del (self[key])
68
+
69
+
70
+ def filterNX(self):
71
+ """
72
+ Filter out domains without any record
73
+ :return:
74
+ """
75
+ keys = []
76
+ for key, value in self.items():
77
+
78
+ while DNSRecord('<NONE>',0,'<EMPTY>',False, {''}, None) in value:
79
+ value.remove(DNSRecord('<NONE>',0,'<EMPTY>',False, {''}, None))
80
+ if not value:
81
+ keys.append(key)
82
+
83
+ for key in keys:
84
+ del(self[key])
85
+
86
+ def seq(self) -> Generator[DatasourceObject]:
87
+ for key in self.keys():
88
+ do = DatasourceObject(domain=key, DNSData=self[key])
89
+ yield do
90
+
91
+ def as_list(self) -> List[DatasourceObject]:
92
+ return list(self.seq())
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+ import re
3
+ from datetime import datetime, timezone
4
+ import aiodns
5
+ from ptodnes.DNS.record import DNSRecord
6
+ from ptodnes.DNS.dns_record_dict import DNSRecordDict
7
+ from ptodnes.datasources.datasource import DNSRecordGenerator, DatasourceObject
8
+ from ptodnes.metaclasses import Singleton
9
+
10
+ class OdnesDNS(metaclass=Singleton):
11
+ """
12
+ Class to provide DNS queries
13
+ """
14
+ def __init__(self, loop):
15
+ self.__resolver = aiodns.DNSResolver(loop=loop)
16
+ self.__rev4 = re.compile(r"^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$")
17
+ self.__rev6 = re.compile(r'(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))', re.IGNORECASE)
18
+
19
+ async def reverse(self, ip: str) -> list[str]:
20
+ if self.__rev4.match(ip) or self.__rev6.match(ip):
21
+ try:
22
+ res = await self.__resolver.gethostbyaddr(ip)
23
+ return res.aliases
24
+ except aiodns.error.DNSError:
25
+ return []
26
+ return []
27
+
28
+ async def query_one(self, domain: str, domain_data: list[DNSRecord], qtype='ANY', *, print_func=None):
29
+ """
30
+ Query one domain with selected record type, update domain_data with results.
31
+ :param domain: domain to query.
32
+ :param domain_data: domain data to update.
33
+ :param qtype: query type.
34
+ :param print_func: output function.
35
+ """
36
+ try:
37
+ if print_func:
38
+ print_func(f"querying {domain}", clear_to_eol=True, end='\r')
39
+ data = await self.__resolver.query(domain, qtype) #ANY not working on all servers
40
+ preprocessed = []
41
+ if type(data) is not type([]):
42
+ preprocessed.append(data)
43
+ else:
44
+ preprocessed = data
45
+ results = {}
46
+ for response in preprocessed:
47
+ record: DNSRecord
48
+ if response.type in ['A', 'AAAA', 'NS']:
49
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.host, ttl=response.ttl,
50
+ verified=True, record_last_seen = datetime.now(tz=timezone.utc))
51
+ elif response.type in ['CNAME']:
52
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.cname,
53
+ ttl=response.ttl, verified=True, record_last_seen = datetime.now(tz=timezone.utc))
54
+ elif response.type in ['MX']:
55
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.host, ttl=response.ttl,
56
+ priority=response.priority, verified=True,
57
+ record_last_seen = datetime.now(tz=timezone.utc))
58
+ elif response.type in ['PTR']:
59
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.name, ttl=response.ttl,
60
+ verified=True, record_last_seen = datetime.now(tz=timezone.utc))
61
+ elif response.type in ['SOA']:
62
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.nsname, ttl=response.ttl,
63
+ verified=True, rname=response.hostmaster, retry=response.retry,
64
+ expire=response.expires, refresh=response.refresh,
65
+ serial=response.serial, minimum=response.minttl,
66
+ record_last_seen = datetime.now(tz=timezone.utc))
67
+ elif response.type in ['SRV']:
68
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.host, ttl=response.ttl,
69
+ verified=True, record_last_seen = datetime.now(tz=timezone.utc))
70
+ elif response.type in ['TXT']:
71
+ record = DNSRecordGenerator(type=response.type, source="DNS", value=response.text, ttl=response.ttl,
72
+ verified=True, record_last_seen = datetime.now(tz=timezone.utc))
73
+ else:
74
+ continue
75
+ sources = set()
76
+ if DNSRecord('<NONE>',0,'<EMPTY>',False, {''}, None) in domain_data:
77
+ for empty in domain_data:
78
+ sources.update(empty.source)
79
+ record.source.update(sources)
80
+
81
+ if record not in domain_data:
82
+ domain_data.append(record)
83
+ else:
84
+ for i in range(len(domain_data)):
85
+ if domain_data[i] == record:
86
+ record.source.update(domain_data[i].source)
87
+ domain_data[i] = record
88
+ except aiodns.error.DNSError:
89
+ pass
90
+
91
+ async def query(self, domain_list: DNSRecordDict, qtype='ANY', *, print_func=None):
92
+ """
93
+ Query provided domain list with selected record type, update its data with results.
94
+ :param domain_list: domain list to query.
95
+ :param qtype: query type.
96
+ """
97
+ tasks = []
98
+ for domain, info in domain_list.items():
99
+ task = asyncio.create_task(self.query_one(domain, info, qtype, print_func=print_func))
100
+ tasks.append(task)
101
+ await asyncio.gather(*tasks)
102
+
@@ -0,0 +1,71 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+
4
+
5
+ @dataclass
6
+ class DNSRecord:
7
+ """
8
+ Base class for DNS record
9
+ """
10
+ type: str
11
+ ttl: int
12
+ value: str
13
+ verified: bool
14
+ source: set[str]
15
+ record_last_seen: datetime | None
16
+
17
+ @staticmethod
18
+ def dict_factory(x):
19
+ exclude_fields = ("ttl",)
20
+ return {k: (v.isoformat() if isinstance(v, datetime) else list(v) if isinstance(v, set) else v) for (k, v) in x if ((v is not None) and (k not in exclude_fields))}
21
+
22
+ def __eq__(self, other):
23
+ return self.type == other.type and self.value == other.value
24
+
25
+ def __hash__(self):
26
+ return hash((self.type, self.value))
27
+
28
+ @dataclass
29
+ class SOARecord(DNSRecord):
30
+ """
31
+ Class for SOA record
32
+ """
33
+ rname: str = None
34
+ retry: int = None
35
+ minimum: int = None
36
+ refresh: int = None
37
+ expire: int = None
38
+ serial: int = None
39
+
40
+ def __eq__(self, other):
41
+ return super().__eq__(other)
42
+
43
+ def __hash__(self):
44
+ return super().__hash__()
45
+
46
+ @dataclass
47
+ class MXRecord(DNSRecord):
48
+ """
49
+ Class for MX record
50
+ """
51
+ priority: int = 0
52
+
53
+ def __eq__(self, other):
54
+ return super().__eq__(other)
55
+
56
+ def __hash__(self):
57
+ return super().__hash__()
58
+
59
+ @dataclass
60
+ class CAARecord(DNSRecord):
61
+ """
62
+ Class for CAA record
63
+ """
64
+ flag: int = None
65
+ tag: str = None
66
+
67
+ def __eq__(self, other):
68
+ return super().__eq__(other)
69
+
70
+ def __hash__(self):
71
+ return super().__hash__()
@@ -0,0 +1,23 @@
1
+ import asyncio
2
+ import signal
3
+
4
+
5
+ def add_signal_handlers():
6
+ """
7
+ Properly handles SIGINT and SIGTERM signals. Ensures correct end of all coroutines.
8
+ :return:
9
+ """
10
+ loop = asyncio.get_event_loop()
11
+
12
+ async def shutdown(_: signal.Signals) -> None:
13
+ """
14
+ Cancel all running async tasks (other than this one) when called.
15
+ By catching asyncio.CancelledError, any running task can perform
16
+ any necessary cleanup when it's cancelled.
17
+ """
18
+ for task in asyncio.all_tasks(loop):
19
+ if task is not asyncio.current_task(loop):
20
+ task.cancel()
21
+
22
+ for sig in [signal.SIGINT, signal.SIGTERM]:
23
+ loop.add_signal_handler(sig, lambda: asyncio.create_task(shutdown(sig)))
@@ -0,0 +1,17 @@
1
+ import sys
2
+ import asyncio
3
+ from ptodnes.ptodnes import main
4
+
5
+
6
+ def __main__():
7
+ if sys.platform == 'win32':
8
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
9
+ loop = asyncio.new_event_loop()
10
+ try:
11
+ loop.run_until_complete(main(loop))
12
+ finally:
13
+ loop.close()
14
+
15
+
16
+ if __name__ == "__main__":
17
+ __main__()
File without changes
@@ -0,0 +1,48 @@
1
+ import tomllib
2
+ import pathlib
3
+ import os
4
+
5
+ from ptodnes.metaclasses import Singleton
6
+
7
+
8
+ class ConfigProvider(metaclass=Singleton):
9
+ """
10
+ Singleton class that provides access to configuration settings.
11
+ """
12
+ @property
13
+ def config_file(self):
14
+ """
15
+ Config file location.
16
+ """
17
+ return self.__config_file
18
+
19
+ @config_file.setter
20
+ def config_file(self, value):
21
+ self.__config_file = value
22
+ if not pathlib.Path.exists(pathlib.Path(value)):
23
+ with open(value, "a", encoding="utf8", newline="") as _: pass
24
+ with open(value, "rb") as f: self.__config = tomllib.load(f)
25
+
26
+ @property
27
+ def config(self):
28
+ """
29
+ Configuration read from configuration file.
30
+ """
31
+ return self.__config
32
+
33
+ def __init__(self, config_file = os.path.join(pathlib.Path.home(), "ptodnes.toml")):
34
+ """
35
+ :param config_file: config file path
36
+ """
37
+ self.__config_file = config_file
38
+ if not pathlib.Path.exists(pathlib.Path(config_file)):
39
+ with open(config_file, "a", encoding="utf8", newline="") as _: pass
40
+ with open(config_file, "rb") as f: self.__config = tomllib.load(f)
41
+
42
+ def get_config(self, section: str) -> dict:
43
+ """
44
+ Get config for specific section.
45
+ :param section: section name
46
+ :return: config
47
+ """
48
+ return self.__config.get(section, {})