ldap-cli 0.2.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.
ldapc/formatter.py ADDED
@@ -0,0 +1,282 @@
1
+ """Formatter module for ldapc.
2
+
3
+ Provides LDAP search filter construction and output formatting functions
4
+ for user and group entries. Supports aligned key-value text, JSON, and
5
+ YAML output formats.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import yaml
13
+
14
+ from ldapc.ldap_client import LdapEntry
15
+
16
+
17
+ def build_user_filter(search_term: str) -> str:
18
+ """Construct an LDAP filter for user search (cn and uid).
19
+
20
+ Builds an OR filter that matches the search term as a substring
21
+ against both the common name (cn) and user ID (uid) attributes.
22
+
23
+ Args:
24
+ search_term: The term to search for in cn and uid fields.
25
+
26
+ Returns:
27
+ An LDAP filter string matching cn or uid.
28
+
29
+ Example:
30
+ >>> build_user_filter("john")
31
+ '(|(cn=*john*)(uid=*john*))'
32
+ """
33
+ return f"(|(cn=*{search_term}*)(uid=*{search_term}*))"
34
+
35
+
36
+ def build_group_filter(search_term: str) -> str:
37
+ """Construct an LDAP filter for group search (cn).
38
+
39
+ Builds a filter that matches the search term as a substring
40
+ against the common name (cn) attribute.
41
+
42
+ Args:
43
+ search_term: The term to search for in the cn field.
44
+
45
+ Returns:
46
+ An LDAP filter string matching cn.
47
+
48
+ Example:
49
+ >>> build_group_filter("admin")
50
+ '(cn=*admin*)'
51
+ """
52
+ return f"(cn=*{search_term}*)"
53
+
54
+
55
+ def format_user_entries(entries: list[LdapEntry]) -> str:
56
+ """Format user entries as aligned key-value text.
57
+
58
+ Displays each user entry's distinguished name, common name, email,
59
+ and group memberships. Multiple entries are separated by a delimiter
60
+ line. Keys are padded to a consistent width for alignment.
61
+
62
+ Args:
63
+ entries: List of LdapEntry objects representing user entries.
64
+
65
+ Returns:
66
+ A formatted string with aligned key-value pairs.
67
+ """
68
+ if not entries:
69
+ return ""
70
+
71
+ blocks: list[str] = []
72
+ for entry in entries:
73
+ lines = _format_user_block(entry)
74
+ blocks.append("\n".join(lines))
75
+
76
+ return "\n---\n".join(blocks)
77
+
78
+
79
+ def format_group_entries(entries: list[LdapEntry]) -> str:
80
+ """Format group entries as aligned key-value text.
81
+
82
+ Displays each group entry's distinguished name, common name,
83
+ description, and member list. Multiple entries are separated by a
84
+ delimiter line. Keys are padded to a consistent width for alignment.
85
+
86
+ Args:
87
+ entries: List of LdapEntry objects representing group entries.
88
+
89
+ Returns:
90
+ A formatted string with aligned key-value pairs.
91
+ """
92
+ if not entries:
93
+ return ""
94
+
95
+ blocks: list[str] = []
96
+ for entry in entries:
97
+ lines = _format_group_block(entry)
98
+ blocks.append("\n".join(lines))
99
+
100
+ return "\n---\n".join(blocks)
101
+
102
+
103
+ def format_entries_json(entries: list[LdapEntry]) -> str:
104
+ """Format entries as a JSON string.
105
+
106
+ Serializes a list of LdapEntry objects into a JSON array where each
107
+ element has "dn" and "attributes" keys.
108
+
109
+ Args:
110
+ entries: List of LdapEntry objects to serialize.
111
+
112
+ Returns:
113
+ A valid JSON string representing the entries.
114
+ """
115
+ data = [
116
+ {"dn": entry.dn, "attributes": entry.attributes}
117
+ for entry in entries
118
+ ]
119
+ return json.dumps(data, indent=2)
120
+
121
+
122
+ def _format_user_block(entry: LdapEntry) -> list[str]:
123
+ """Format a single user entry as aligned key-value lines.
124
+
125
+ Args:
126
+ entry: An LdapEntry representing a user.
127
+
128
+ Returns:
129
+ A list of formatted lines.
130
+ """
131
+ cn = _get_first_attr(entry, "cn")
132
+ mail = _get_first_attr(entry, "mail")
133
+ groups = entry.attributes.get("memberOf", [])
134
+
135
+ key_width = 10 # Enough for "memberOf" + padding
136
+
137
+ lines = [
138
+ f"{'DN':<{key_width}}: {entry.dn}",
139
+ f"{'CN':<{key_width}}: {cn}",
140
+ f"{'Email':<{key_width}}: {mail}",
141
+ ]
142
+
143
+ if groups:
144
+ lines.append(f"{'Groups':<{key_width}}: {groups[0]}")
145
+ for group in groups[1:]:
146
+ lines.append(f"{'':<{key_width}} {group}")
147
+ else:
148
+ lines.append(f"{'Groups':<{key_width}}: ")
149
+
150
+ return lines
151
+
152
+
153
+ def _format_group_block(entry: LdapEntry) -> list[str]:
154
+ """Format a single group entry as aligned key-value lines.
155
+
156
+ Args:
157
+ entry: An LdapEntry representing a group.
158
+
159
+ Returns:
160
+ A list of formatted lines.
161
+ """
162
+ cn = _get_first_attr(entry, "cn")
163
+ description = _get_first_attr(entry, "description")
164
+ members = entry.attributes.get("member", [])
165
+
166
+ key_width = 12 # Enough for "Description" + padding
167
+
168
+ lines = [
169
+ f"{'DN':<{key_width}}: {entry.dn}",
170
+ f"{'CN':<{key_width}}: {cn}",
171
+ f"{'Description':<{key_width}}: {description}",
172
+ ]
173
+
174
+ if members:
175
+ lines.append(f"{'Members':<{key_width}}: {members[0]}")
176
+ for member in members[1:]:
177
+ lines.append(f"{'':<{key_width}} {member}")
178
+ else:
179
+ lines.append(f"{'Members':<{key_width}}: ")
180
+
181
+ return lines
182
+
183
+
184
+ def _get_first_attr(entry: LdapEntry, attr_name: str) -> str:
185
+ """Get the first value of an attribute, or empty string if missing.
186
+
187
+ Args:
188
+ entry: The LdapEntry to read from.
189
+ attr_name: The attribute name to look up.
190
+
191
+ Returns:
192
+ The first value of the attribute, or an empty string.
193
+ """
194
+ values = entry.attributes.get(attr_name, [])
195
+ return values[0] if values else ""
196
+
197
+
198
+ def _extract_cn(dn: str) -> str:
199
+ """Extract the cn value from a DN string.
200
+
201
+ Args:
202
+ dn: A distinguished name like 'cn=admins,ou=groups,dc=example,dc=com'.
203
+
204
+ Returns:
205
+ The cn value (e.g. 'admins'), or the full DN if no cn= found.
206
+ """
207
+ for part in dn.split(","):
208
+ part = part.strip()
209
+ if part.lower().startswith("cn="):
210
+ return part[3:]
211
+ return dn
212
+
213
+
214
+ def format_user_entries_yaml(entries: list[LdapEntry]) -> str:
215
+ """Format user entries as YAML.
216
+
217
+ Each entry becomes a YAML document with cn, email, and groups
218
+ (groups shown as just their cn component).
219
+
220
+ Args:
221
+ entries: List of LdapEntry objects representing user entries.
222
+
223
+ Returns:
224
+ A YAML-formatted string.
225
+ """
226
+ if not entries:
227
+ return ""
228
+
229
+ docs: list[str] = []
230
+ for entry in entries:
231
+ cn = _get_first_attr(entry, "cn")
232
+ mail = _get_first_attr(entry, "mail")
233
+ groups = entry.attributes.get("memberOf", [])
234
+
235
+ data: dict = {"cn": cn}
236
+ if mail:
237
+ data["email"] = mail
238
+ if groups:
239
+ data["groups"] = [_extract_cn(g) for g in groups]
240
+ else:
241
+ data["groups"] = []
242
+
243
+ docs.append(yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip())
244
+
245
+ return "---\n" + "\n---\n".join(docs)
246
+
247
+
248
+ def format_group_entries_yaml(entries: list[LdapEntry]) -> str:
249
+ """Format group entries as YAML.
250
+
251
+ Each entry becomes a YAML document with cn, description, and members
252
+ (members shown as just their cn or uid component).
253
+
254
+ Args:
255
+ entries: List of LdapEntry objects representing group entries.
256
+
257
+ Returns:
258
+ A YAML-formatted string.
259
+ """
260
+ if not entries:
261
+ return ""
262
+
263
+ docs: list[str] = []
264
+ for entry in entries:
265
+ cn = _get_first_attr(entry, "cn")
266
+ description = _get_first_attr(entry, "description")
267
+ members = entry.attributes.get("member", [])
268
+ member_uids = entry.attributes.get("memberUid", [])
269
+
270
+ data: dict = {"cn": cn}
271
+ if description:
272
+ data["description"] = description
273
+ if members:
274
+ data["members"] = [_extract_cn(m) for m in members]
275
+ elif member_uids:
276
+ data["members"] = member_uids
277
+ else:
278
+ data["members"] = []
279
+
280
+ docs.append(yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip())
281
+
282
+ return "---\n" + "\n---\n".join(docs)