facterpy 1.0.0__py3-none-any.whl

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.
facter/__init__.py ADDED
@@ -0,0 +1,218 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import subprocess
5
+ from typing import Any, Dict, Generator, Iterator, Optional, Tuple, Union
6
+
7
+ log = logging.getLogger("facter")
8
+
9
+
10
+ def _parse_cli_facter_results(
11
+ facter_results: str,
12
+ ) -> Generator[Tuple[str, str], None, None]:
13
+ '''Parse key value pairs printed with "=>" separators.
14
+ Used as fallback when JSON output is not available.
15
+
16
+ >>> list(_parse_cli_facter_results("""foo => bar
17
+ ... baz => 1
18
+ ... foo_bar => True"""))
19
+ [('foo', 'bar'), ('baz', '1'), ('foo_bar', 'True')]
20
+ >>> list(_parse_cli_facter_results("""foo => bar
21
+ ... babababababababab
22
+ ... baz => 2"""))
23
+ [('foo', 'bar\nbabababababababab'), ('baz', '2')]
24
+ >>> list(_parse_cli_facter_results("""3434"""))
25
+ Traceback (most recent call last):
26
+ ...
27
+ ValueError: parse error
28
+
29
+
30
+ Uses a generator interface:
31
+ >>> _parse_cli_facter_results("foo => bar").next()
32
+ ('foo', 'bar')
33
+ '''
34
+ last_key, last_value = None, []
35
+ for line in filter(None, facter_results.splitlines()):
36
+ res = line.split(" => ", 1)
37
+ if len(res) == 1:
38
+ if not last_key:
39
+ raise ValueError("parse error")
40
+ # Continue multiline value
41
+ last_value.append(res[0]) # type: ignore[unreachable] # mypy 3.8 compat
42
+ else:
43
+ if last_key:
44
+ yield last_key, os.linesep.join(last_value) # type: ignore[unreachable] # mypy 3.8 compat
45
+ last_key, last_value = res[0], [res[1]]
46
+
47
+ # Yield final key-value pair if exists
48
+ if last_key:
49
+ yield last_key, os.linesep.join(last_value)
50
+
51
+
52
+ class Facter:
53
+ def __init__(
54
+ self,
55
+ facter_path: str = "facter",
56
+ external_dir: Optional[str] = None,
57
+ cache_enabled: bool = True,
58
+ puppet_facts: bool = False,
59
+ legacy_facts: bool = False,
60
+ # Deprecated - kept for backward compatibility
61
+ use_yaml: Optional[bool] = None,
62
+ ) -> None:
63
+ self.facter_path = facter_path
64
+ self.external_dir = external_dir
65
+ self.cache_enabled = cache_enabled
66
+ self.puppet_facts = puppet_facts
67
+ self.legacy_facts = legacy_facts
68
+ self._cache: Optional[Dict[str, Any]] = None
69
+
70
+ # Handle deprecated use_yaml parameter
71
+ if use_yaml is not None:
72
+ log.warning(
73
+ "The 'use_yaml' parameter is deprecated. "
74
+ "facterpy now uses JSON by default with text fallback."
75
+ )
76
+
77
+ @property
78
+ def uses_yaml(self) -> bool:
79
+ """Deprecated property. facterpy now uses JSON by default."""
80
+ log.warning(
81
+ "The 'uses_yaml' property is deprecated. "
82
+ "facterpy now uses JSON by default with text fallback."
83
+ )
84
+ return False
85
+
86
+ def run_facter(self, key: Optional[str] = None) -> Union[Dict[str, Any], Any]:
87
+ """Run the facter executable with an optional specific fact.
88
+
89
+ Uses JSON output by default (facter 3.0+) with fallback to plain text parsing.
90
+ Returns a dictionary if no key is given, and the value if a key is passed.
91
+
92
+ If legacy_facts=True, includes legacy facts (like facter --show-legacy).
93
+ This is required for lookup() to find legacy facts like 'architecture' that
94
+ don't appear in modern structured fact output but work with individual
95
+ facter commands (e.g., 'facter architecture').
96
+ """
97
+ base_args = [self.facter_path]
98
+
99
+ # Add common arguments
100
+ if self.puppet_facts:
101
+ base_args.append("--puppet")
102
+ if self.external_dir is not None:
103
+ base_args.extend(["--external-dir", self.external_dir])
104
+ if self.legacy_facts:
105
+ base_args.append("--show-legacy")
106
+ if key is not None:
107
+ base_args.append(key)
108
+
109
+ # Try JSON first (preferred)
110
+ json_args = base_args + ["--json"]
111
+ try:
112
+ proc = subprocess.Popen(
113
+ json_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
114
+ )
115
+ stdout, stderr = proc.communicate()
116
+ if proc.returncode == 0:
117
+ results = stdout.decode()
118
+ parsed_results = json.loads(results)
119
+ if key is not None:
120
+ return parsed_results.get(key)
121
+ return parsed_results
122
+ except (json.JSONDecodeError, FileNotFoundError, subprocess.SubprocessError):
123
+ # Fall back to text parsing
124
+ pass
125
+
126
+ # Fallback to plain text output
127
+ try:
128
+ proc = subprocess.Popen(
129
+ base_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
130
+ )
131
+ stdout, stderr = proc.communicate()
132
+ if proc.returncode != 0:
133
+ raise RuntimeError(f"facter command failed: {stderr.decode()}")
134
+ results = stdout.decode()
135
+ if key is not None:
136
+ return results.strip()
137
+ return dict(_parse_cli_facter_results(results))
138
+ except (FileNotFoundError, subprocess.SubprocessError):
139
+ log.exception("Facter execution failed")
140
+ raise
141
+
142
+ def build_cache(self) -> None:
143
+ """run facter and save the results to `_cache`"""
144
+ cache = self.run_facter()
145
+ self._cache = cache if isinstance(cache, dict) else {}
146
+
147
+ def clear_cache(self) -> None:
148
+ self._cache = None
149
+
150
+ def has_cache(self) -> bool:
151
+ """Intended to be called before any call that might access the
152
+ cache. If the cache is not selected, then returns False,
153
+ otherwise the cache is build if needed and returns True."""
154
+ if not self.cache_enabled:
155
+ return False
156
+ if self._cache is None:
157
+ self.build_cache()
158
+ return True
159
+
160
+ def lookup(self, fact: str, cache: bool = True) -> Any:
161
+ """Return the value of a given fact and raise a KeyError if
162
+ it is not available. If `cache` is False, force the lookup of
163
+ the fact."""
164
+ if (not cache) or (not self.has_cache()):
165
+ val = self.run_facter(fact)
166
+ if val is None or val == "":
167
+ raise KeyError(fact)
168
+ return val
169
+ if self._cache is None:
170
+ raise RuntimeError("Cache is None but has_cache returned True")
171
+ return self._cache[fact]
172
+
173
+ def get(self, k: str, d: Any = None) -> Any:
174
+ """Dictionary-like `get` method with a default value"""
175
+ try:
176
+ return self.lookup(k)
177
+ except KeyError:
178
+ return d
179
+
180
+ @property
181
+ def all(self) -> Dict[str, Any]:
182
+ """Dictionary representation of all facts"""
183
+ if not self.has_cache():
184
+ result = self.run_facter()
185
+ return result if isinstance(result, dict) else {}
186
+ return self._cache or {}
187
+
188
+ def keys(self) -> Iterator[str]:
189
+ return iter(self.all.keys())
190
+
191
+ def values(self) -> Iterator[Any]:
192
+ return iter(self.all.values())
193
+
194
+ def items(self) -> Iterator[Tuple[str, Any]]:
195
+ return iter(self.all.items())
196
+
197
+ def __getitem__(self, key: str) -> Any:
198
+ return self.lookup(key)
199
+
200
+ def __iter__(self) -> Iterator[str]:
201
+ return self.keys()
202
+
203
+ def __repr__(self) -> str:
204
+ return (
205
+ f"<Facter cache_enabled={self.cache_enabled!r} "
206
+ f"cache_active={self._cache is not None!r}>"
207
+ )
208
+
209
+ def json(self) -> str:
210
+ """Return a json dump of all facts"""
211
+ return json.dumps(self.all)
212
+
213
+
214
+ _FACTER = Facter()
215
+
216
+
217
+ def get_fact(fact: str, default: Any = None) -> Any:
218
+ return _FACTER.get(fact, default)
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.4
2
+ Name: facterpy
3
+ Version: 1.0.0
4
+ Summary: Python library to provide a cached and dictionary-like interface to Puppet's facter utility
5
+ Author: Kali Norby
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/knorby/facterpy
8
+ Project-URL: Repository, https://github.com/knorby/facterpy
9
+ Project-URL: Issues, https://github.com/knorby/facterpy/issues
10
+ Keywords: facter,puppet,ruby,system,facts
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: License :: OSI Approved :: BSD License
13
+ Classifier: Topic :: System :: Systems Administration
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: Operating System :: POSIX
16
+ Classifier: Operating System :: MacOS :: MacOS X
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.8
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Requires-Python: >=3.8
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Provides-Extra: dev
28
+ Requires-Dist: black; extra == "dev"
29
+ Requires-Dist: ruff; extra == "dev"
30
+ Requires-Dist: mypy; extra == "dev"
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-cov; extra == "dev"
33
+ Requires-Dist: pre-commit; extra == "dev"
34
+ Requires-Dist: tox; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ facterpy
38
+ ========
39
+
40
+ Python library to provide a cached and dictionary-like interface to [Puppet's facter utility](http://puppetlabs.com/puppet/related-projects/facter).
41
+
42
+ The library uses JSON output by default (facter 3.0+) with automatic fallback to plain text parsing for maximum compatibility and performance.
43
+
44
+ Usage
45
+ -----
46
+
47
+ ```python
48
+ >>> import facter
49
+ >>> f = facter.Facter()
50
+ >>> f["architecture"]
51
+ 'x86_64'
52
+ >>> f.lookup("uptime_seconds")
53
+ 195106
54
+ >>> f.lookup("uptime_seconds") # cached result
55
+ 195106
56
+ >>> f.lookup("uptime_seconds", cache=False) # force refresh
57
+ 195234
58
+ >>> f.get("not_a_fact", "default_value")
59
+ 'default_value'
60
+ >>> f.all # get all facts as dictionary
61
+ {'architecture': 'x86_64', 'uptime_seconds': 195234, ...}
62
+ ```
63
+
64
+ ### Advanced Usage
65
+
66
+ ```python
67
+ # Custom facter path
68
+ f = facter.Facter(facter_path="/usr/local/bin/facter")
69
+
70
+ # External facts directory
71
+ f = facter.Facter(external_dir="/etc/puppetlabs/facter/facts.d")
72
+
73
+ # Include Puppet facts
74
+ f = facter.Facter(puppet_facts=True)
75
+
76
+ # Disable caching
77
+ f = facter.Facter(cache_enabled=False)
78
+
79
+ # Enable legacy facts (equivalent to facter --show-legacy)
80
+ f = facter.Facter(legacy_facts=True)
81
+ f.lookup("architecture") # Works with legacy facts enabled
82
+ f["operatingsystem"] # Legacy facts appear in f.all
83
+ ```
84
+
85
+ Install
86
+ -------
87
+
88
+ ```bash
89
+ pip install facterpy
90
+ ```
91
+
92
+ Requirements
93
+ ------------
94
+
95
+ **Required:**
96
+ - Python 3.8+
97
+ - `facter` command-line utility (install via system packages or Puppet)
98
+
99
+ **No external Python dependencies** - uses only Python standard library.
100
+
101
+ Compatibility
102
+ -------------
103
+
104
+ - **Python**: 3.8+ (Python 2 support removed in v0.2.0)
105
+ - **Facter**: 3.0+ (JSON output), with fallback support for older versions
106
+ - **Platforms**: Linux, macOS, and other POSIX systems
107
+
108
+ ### Legacy Facts
109
+
110
+ Modern facter (4.x+) uses structured facts, so legacy top-level facts like `architecture` are nested under structured facts like `os.architecture`. To access legacy facts that were available in older facter versions:
111
+
112
+ ```python
113
+ # Modern behavior (default) - structured facts only
114
+ f = facter.Facter()
115
+ f.lookup("architecture") # Raises KeyError - not in structured output
116
+ f.all["os"]["architecture"] # Works - nested in structured facts
117
+
118
+ # Legacy behavior - includes legacy facts (like facter --show-legacy)
119
+ f = facter.Facter(legacy_facts=True)
120
+ f.lookup("architecture") # Works - legacy fact available
121
+ f["architecture"] # Works - appears in f.all output
122
+ ```
123
+
124
+ Migration from v0.1.x
125
+ ---------------------
126
+
127
+ **Version 1.0.0 represents a major modernization** while maintaining API compatibility. This version bump reflects the significant gap since the last release (12+ years) and commitment to not breaking existing code.
128
+
129
+ - **Breaking changes**: Python 2 support removed, PyYAML dependency removed
130
+ - **Modernization**: Complete rewrite with JSON-first approach, type hints, modern tooling
131
+ - **API stability**: Core API unchanged to preserve compatibility with existing code
132
+
133
+ **Migration notes:**
134
+
135
+ ```python
136
+ # Old (deprecated, shows warning)
137
+ f = facter.Facter(use_yaml=False)
138
+
139
+ # New (recommended)
140
+ f = facter.Facter() # Automatically uses JSON with text fallback
141
+
142
+ # For legacy fact compatibility (if needed)
143
+ f = facter.Facter(legacy_facts=True) # Includes pre-4.x style facts
144
+ ```
145
+
146
+ Project State
147
+ -------------
148
+
149
+ I wrote this library in 2013 and did very little maintenance since then, despite some apparent usage. The library is simple and focused, which has helped it remain functional. This 1.0.0 modernization brings it up to current standards while preserving the original API. I haven't used Puppet in quite some time, so I'm not an active user of this library, but the comprehensive test suite should help ensure reliability.
@@ -0,0 +1,6 @@
1
+ facter/__init__.py,sha256=iFF4EOHKfMkuvLAOm5xGuyIVBBdnRE33Kk3HowN_iRo,7556
2
+ facterpy-1.0.0.dist-info/licenses/LICENSE,sha256=goHNs7xDDii7xgwaX1TPhEAKGIb7KdvkouwGQlF7Lsc,1283
3
+ facterpy-1.0.0.dist-info/METADATA,sha256=2ZfN9GguL5GBk2oxxrp8DQ3W5G8T6BYnlpCG0eu-Fn8,5139
4
+ facterpy-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ facterpy-1.0.0.dist-info/top_level.txt,sha256=-x6G-A1aImWfM5qqTno8sLrEugOXqvSmXbOrq7bgfwk,7
6
+ facterpy-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2013, Kali Norby
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are
6
+ met:
7
+
8
+ Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer. Redistributions
10
+ in binary form must reproduce the above copyright notice, this list of
11
+ conditions and the following disclaimer in the documentation and/or
12
+ other materials provided with the distribution. THIS SOFTWARE IS
13
+ PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
14
+ EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16
+ PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
17
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
21
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
22
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ facter