19.1. Recap About
19.1.1. Assignments
# %% About
# - Name: Recap About Records
# - Difficulty: easy
# - Lines: 2
# - Minutes: 3
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Define function `read()` which returns content of a file:
# - parameter: `filename: str`, `encoding: str`
# - returns: `str` - content of the file
# 2. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj funkcję `read()` która zwraca zawartość pliku:
# - parametr: `filename: str`, `encoding: str`
# - zwraca: `str` - treść pliku
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% References
# [1] passwd(5) - File Formats Manual
# Year: 2017
# Retrieved: 2025-02-14
# URL: https://manpages.debian.org/unstable/passwd/passwd.5.en.html
# %% Example
# >>> read(DATA)
# """# File: /etc/passwd
# root:x:0:0:root:/root:/bin/bash
# daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
# bin:x:2:2:bin:/bin:/usr/sbin/nologin
# sys:x:3:3:sys:/dev:/usr/sbin/nologin
# alice:x:1000:1000:Alice:/home/alice:/bin/bash
# bob:x:1001:1001:Bob:/home/bob:/bin/bash
# carol:x:1002:1002:Carol:/home/carol:/bin/bash
# dave:x:1003:1003:Dave:/home/dave:/bin/bash
# eve:x:1004:1004:Eve:/home/eve:/bin/bash
# mallory:x:1005:1005:Mallory:/home/mallory:/bin/bash
# nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
# """
# <BLANKLINE>
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> result = read(FILE)
>>> print(result)
# File: /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
alice:x:1000:1000:Alice:/home/alice:/bin/bash
bob:x:1001:1001:Bob:/home/bob:/bin/bash
carol:x:1002:1002:Carol:/home/carol:/bin/bash
dave:x:1003:1003:Dave:/home/dave:/bin/bash
eve:x:1004:1004:Eve:/home/eve:/bin/bash
mallory:x:1005:1005:Mallory:/home/mallory:/bin/bash
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
<BLANKLINE>
>>> from os import remove
>>> remove(FILE)
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
asaccounts: Callable[[str, str], str]
# %% Data
FILE = '_temporary.txt'
with open(FILE, mode='wt', encoding='utf-8') as file:
file.write("""# File: /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
alice:x:1000:1000:Alice:/home/alice:/bin/bash
bob:x:1001:1001:Bob:/home/bob:/bin/bash
carol:x:1002:1002:Carol:/home/carol:/bin/bash
dave:x:1003:1003:Dave:/home/dave:/bin/bash
eve:x:1004:1004:Eve:/home/eve:/bin/bash
mallory:x:1005:1005:Mallory:/home/mallory:/bin/bash
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
""")
# %% Result
def read(filename, encoding='utf-8'):
return ...
# %% About
# - Name: Recap About Records
# - Difficulty: easy
# - Lines: 9
# - Minutes: 8
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Define function `asrows()` that converts `str` into `list[tuple[str,...]]`:
# - parameter: `data: str`
# - returns: `list[tuple[str,...]]`
# 2. Function must:
# - skip empty lines
# - skip comments (lines starting with `#`)
# - split lines by colon (`:`)
# - return a list of tuples
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj funkcję `asrows()` która przekształci `str` na `list[tuple[str,...]]`:
# - parametr: `data: str`
# - zwraca: `list[tuple[str,...]]`
# 2. Funkcja musi:
# - pomijać puste linie
# - pomijać komentarze (linie zaczynające się od `#`)
# - dzielić linie po dwukropku (`:`)
# - zwracać listę krotek
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% References
# [1] passwd(5) - File Formats Manual
# Year: 2017
# Retrieved: 2025-02-14
# URL: https://manpages.debian.org/unstable/passwd/passwd.5.en.html
# %% Example
# >>> asrows(DATA)
# [('root', 'x', '0', '0', 'root', '/root', '/bin/bash'),
# ('daemon', 'x', '1', '1', 'daemon', '/usr/sbin', '/usr/sbin/nologin'),
# ('bin', 'x', '2', '2', 'bin', '/bin', '/usr/sbin/nologin'),
# ('sys', 'x', '3', '3', 'sys', '/dev', '/usr/sbin/nologin'),
# ('alice', 'x', '1000', '1000', 'Alice', '/home/alice', '/bin/bash'),
# ('bob', 'x', '1001', '1001', 'Bob', '/home/bob', '/bin/bash'),
# ('carol', 'x', '1002', '1002', 'Carol', '/home/carol', '/bin/bash'),
# ('dave', 'x', '1003', '1003', 'Dave', '/home/dave', '/bin/bash'),
# ('eve', 'x', '1004', '1004', 'Eve', '/home/eve', '/bin/bash'),
# ('mallory', 'x', '1005', '1005', 'Mallory', '/home/mallory', '/bin/bash'),
# ('nobody', 'x', '65534', '65534', 'nobody', '/nonexistent', '/usr/sbin/nologin')]
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 12), \
'Python 3.12+ required'
>>> from pprint import pprint
>>> result = asrows(DATA)
>>> pprint(result, width=90)
[('root', 'x', '0', '0', 'root', '/root', '/bin/bash'),
('daemon', 'x', '1', '1', 'daemon', '/usr/sbin', '/usr/sbin/nologin'),
('bin', 'x', '2', '2', 'bin', '/bin', '/usr/sbin/nologin'),
('sys', 'x', '3', '3', 'sys', '/dev', '/usr/sbin/nologin'),
('alice', 'x', '1000', '1000', 'Alice', '/home/alice', '/bin/bash'),
('bob', 'x', '1001', '1001', 'Bob', '/home/bob', '/bin/bash'),
('carol', 'x', '1002', '1002', 'Carol', '/home/carol', '/bin/bash'),
('dave', 'x', '1003', '1003', 'Dave', '/home/dave', '/bin/bash'),
('eve', 'x', '1004', '1004', 'Eve', '/home/eve', '/bin/bash'),
('mallory', 'x', '1005', '1005', 'Mallory', '/home/mallory', '/bin/bash'),
('nobody', 'x', '65534', '65534', 'nobody', '/nonexistent', '/usr/sbin/nologin')]
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
type result = list[tuple[str, str, str, str, str, str, str]]
asrows: Callable[[str,str,str], result]
# %% Data
DATA = """# File: /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
alice:x:1000:1000:Alice:/home/alice:/bin/bash
bob:x:1001:1001:Bob:/home/bob:/bin/bash
carol:x:1002:1002:Carol:/home/carol:/bin/bash
dave:x:1003:1003:Dave:/home/dave:/bin/bash
eve:x:1004:1004:Eve:/home/eve:/bin/bash
mallory:x:1005:1005:Mallory:/home/mallory:/bin/bash
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
"""
# %% Result
def asrows(data, separator=':', comment='#'):
return ...
# %% About
# - Name: Recap About EtcPasswdUsername
# - Difficulty: easy
# - Lines: 6
# - Minutes: 5
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Define function `asaccounts()` that returns `login` and `uid` of each user:
# - parameter: `data: list[tuple[str,...]]`
# - returns: `list[tuple[str, int]]`
# 2. The function must:
# - extract `login` and `uid` (user identification number)
# - convert `uid` to an integer
# - return a tuple with `login` and `uid` for each user
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj funkcję `asaccounts()` która zwróci `login` i `uid` każdego użytkownika:
# - parametr: `data: list[tuple[str,...]]`
# - zwraca: `list[tuple[str, int]]`
# 2. Funkcja musi:
# - wyodrębnić `login` i `uid` (numer identyfikacyjny użytkownika)
# - przekonwertować `uid` na liczbę całkowitą
# - zwrócić krotkę z `login` i `uid` dla każdego użytkownika
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% References
# [1] passwd(5) - File Formats Manual
# Year: 2017
# Retrieved: 2025-02-14
# URL: https://manpages.debian.org/unstable/passwd/passwd.5.en.html
# %% Example
# >>> asaccounts(DATA)
# [('root', 0),
# ('daemon', 1),
# ('bin', 2),
# ('sys', 3),
# ('alice', 1000),
# ('bob', 1001),
# ('carol', 1002),
# ('dave', 1003),
# ('eve', 1004),
# ('mallory', 1005),
# ('nobody', 65534)]
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 12), \
'Python 3.12+ required'
>>> from pprint import pprint
>>> result = asaccounts(DATA)
>>> pprint(result)
[('root', 0),
('daemon', 1),
('bin', 2),
('sys', 3),
('alice', 1000),
('bob', 1001),
('carol', 1002),
('dave', 1003),
('eve', 1004),
('mallory', 1005),
('nobody', 65534)]
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
type data = list[tuple[str, str, str, str, str, str, str]]
type result = list[tuple[str, int]]
asaccounts: Callable[[data], result]
# %% Data
DATA = [
# File: /etc/passwd
('root', 'x', '0', '0', 'root', '/root', '/bin/bash'),
('daemon', 'x', '1', '1', 'daemon', '/usr/sbin', '/usr/sbin/nologin'),
('bin', 'x', '2', '2', 'bin', '/bin', '/usr/sbin/nologin'),
('sys', 'x', '3', '3', 'sys', '/dev', '/usr/sbin/nologin'),
('alice', 'x', '1000', '1000', 'Alice', '/home/alice', '/bin/bash'),
('bob', 'x', '1001', '1001', 'Bob', '/home/bob', '/bin/bash'),
('carol', 'x', '1002', '1002', 'Carol', '/home/carol', '/bin/bash'),
('dave', 'x', '1003', '1003', 'Dave', '/home/dave', '/bin/bash'),
('eve', 'x', '1004', '1004', 'Eve', '/home/eve', '/bin/bash'),
('mallory', 'x', '1005', '1005', 'Mallory', '/home/mallory', '/bin/bash'),
('nobody', 'x', '65534', '65534', 'nobody', '/nonexistent', '/usr/sbin/nologin'),
]
# %% Result
def asaccounts(data):
"""
Structure of `/etc/passwd` file:
| Field | Type | Description |
|----------|------|-----------------------------------|
| login | str | login name |
| password | str | optional encrypted password |
| uid | int | numerical user ID |
| gid | int | numerical group ID |
| comment | str | user name or comment field |
| home | str | user home directory |
| shell | str | optional user command interpreter |
If the password field is a lower-case 'x', then the encrypted
password is actually stored in the `/etc/shadow` file instead
Example:
- alice:x:1000:1000:Alice:/home/alice:/bin/bash
- [login]:[password]:[uid]:[gid]:[comment]:[home]:[shell]
"""
return ...
# %% About
# - Name: Recap About EtcPasswd
# - Difficulty: medium
# - Lines: 12
# - Minutes: 13
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Define function `aspasswd()` that converts data:
# - parameter: `data: list[tuple[str,...]`
# - returns: `dict[str,dict]`
# 2. The function returns a dictionary with data for each user:
# - the key in the dictionary is the `login` of the user
# - the value in the dictionary is another dictionary with data
# - example: {'alice': {...}, 'bob': {...}, 'carol': {...}}
# - convert `uid` and `gid` to integers
# 3. Record dictionary keys:
# - `user_login: str` - username
# - `user_password: str` - `x` indicates that shadow passwords are used
# - `user_uid: int` - user identification number
# - `user_gid: int` - group identification number
# - `user_comment: str` - comment ie. full name
# - `user_home: str` - path to home directory
# - `user_shell: str` - path to program run at login
# 4. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj funkcję `aspasswd()` która przekształci dane:
# - parametr: `data: list[tuple[str,...]`
# - zwraca: `dict[str,dict]`
# 2. Funkcja zwraca słownik z danymi dla każdego użytkownika:
# - kluczem w słowniku jest `login` użytkownika
# - wartością w słowniku jest kolejny słownik z danymi
# - przykład: {'alice': {...}, 'bob': {...}, 'carol': {...}}
# - przekonwertuj `uid` i `gid` na liczby całkowite
# 3. Klucze słownika z danymi:
# - `user_login: str` - nazwa użytkownika
# - `user_password: str` - `x` znaczy, że hasło jest w pliku shadow
# - `user_uid: int` - numer identyfikacyjny użytkownika
# - `user_gid: int` - numer identyfikacyjny grupy
# - `user_comment: str` - komentarz, np. imię i nazwisko
# - `user_home: str` - ścieżka do katalogu domowego
# - `user_shell: str` - ścieżka do programu uruchamianego po zalogowaniu
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% Example
# >>> aspasswd(DATA)
# 'alice': {'user_comment': 'Alice',
# 'user_gid': 1000,
# 'user_home': '/home/alice',
# 'user_login': 'alice',
# 'user_password': 'x',
# 'user_shell': '/bin/bash',
# 'user_uid': 1000},
# 'bob': {'user_comment': 'Bob',
# 'user_gid': 1001,
# 'user_home': '/home/bob',
# 'user_login': 'bob',
# 'user_password': 'x',
# 'user_shell': '/bin/bash',
# 'user_uid': 1001},
# 'carol': {'user_comment': 'Carol',
# 'user_gid': 1002,
# 'user_home': '/home/carol',
# 'user_login': 'carol',
# 'user_password': 'x',
# 'user_shell': '/bin/bash',
# 'user_uid': 1002},
# ...]
# %% Hints
# - `len()`
# - `int()`
# - `str.splitlines()`
# - `str.isspace()`
# - `str.startswith()`
# - `str.split()`
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 12), \
'Python 3.12+ required'
>>> from pprint import pprint
>>> result = aspasswd(DATA)
>>> pprint(result, sort_dicts=True)
{'alice': {'user_comment': 'Alice',
'user_gid': 1000,
'user_home': '/home/alice',
'user_login': 'alice',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 1000},
'bin': {'user_comment': 'bin',
'user_gid': 2,
'user_home': '/bin',
'user_login': 'bin',
'user_password': 'x',
'user_shell': '/usr/sbin/nologin',
'user_uid': 2},
'bob': {'user_comment': 'Bob',
'user_gid': 1001,
'user_home': '/home/bob',
'user_login': 'bob',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 1001},
'carol': {'user_comment': 'Carol',
'user_gid': 1002,
'user_home': '/home/carol',
'user_login': 'carol',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 1002},
'daemon': {'user_comment': 'daemon',
'user_gid': 1,
'user_home': '/usr/sbin',
'user_login': 'daemon',
'user_password': 'x',
'user_shell': '/usr/sbin/nologin',
'user_uid': 1},
'dave': {'user_comment': 'Dave',
'user_gid': 1003,
'user_home': '/home/dave',
'user_login': 'dave',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 1003},
'eve': {'user_comment': 'Eve',
'user_gid': 1004,
'user_home': '/home/eve',
'user_login': 'eve',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 1004},
'mallory': {'user_comment': 'Mallory',
'user_gid': 1005,
'user_home': '/home/mallory',
'user_login': 'mallory',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 1005},
'nobody': {'user_comment': 'nobody',
'user_gid': 65534,
'user_home': '/nonexistent',
'user_login': 'nobody',
'user_password': 'x',
'user_shell': '/usr/sbin/nologin',
'user_uid': 65534},
'root': {'user_comment': 'root',
'user_gid': 0,
'user_home': '/root',
'user_login': 'root',
'user_password': 'x',
'user_shell': '/bin/bash',
'user_uid': 0},
'sys': {'user_comment': 'sys',
'user_gid': 3,
'user_home': '/dev',
'user_login': 'sys',
'user_password': 'x',
'user_shell': '/usr/sbin/nologin',
'user_uid': 3}}
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
type data = list[tuple[str, str, str, str, str, str, str]]
type result = dict[str, dict[str, str|int]]
aspasswd: Callable[[data], result]
# %% Data
DATA = [
# File: /etc/passwd
('root', 'x', '0', '0', 'root', '/root', '/bin/bash'),
('daemon', 'x', '1', '1', 'daemon', '/usr/sbin', '/usr/sbin/nologin'),
('bin', 'x', '2', '2', 'bin', '/bin', '/usr/sbin/nologin'),
('sys', 'x', '3', '3', 'sys', '/dev', '/usr/sbin/nologin'),
('alice', 'x', '1000', '1000', 'Alice', '/home/alice', '/bin/bash'),
('bob', 'x', '1001', '1001', 'Bob', '/home/bob', '/bin/bash'),
('carol', 'x', '1002', '1002', 'Carol', '/home/carol', '/bin/bash'),
('dave', 'x', '1003', '1003', 'Dave', '/home/dave', '/bin/bash'),
('eve', 'x', '1004', '1004', 'Eve', '/home/eve', '/bin/bash'),
('mallory', 'x', '1005', '1005', 'Mallory', '/home/mallory', '/bin/bash'),
('nobody', 'x', '65534', '65534', 'nobody', '/nonexistent', '/usr/sbin/nologin'),
]
# %% Result
def aspasswd(data):
"""
Structure of `/etc/passwd` file:
| Field | Type | Description |
|----------|------|-----------------------------------|
| login | str | login name |
| password | str | optional encrypted password |
| uid | int | numerical user ID |
| gid | int | numerical group ID |
| comment | str | user name or comment field |
| home | str | user home directory |
| shell | str | optional user command interpreter |
If the password field is a lower-case 'x', then the encrypted
password is actually stored in the `/etc/shadow` file instead
Example:
- alice:x:1000:1000:Alice:/home/alice:/bin/bash
- [login]:[password]:[uid]:[gid]:[comment]:[home]:[shell]
"""
return {
'user_login': ...,
'user_password': ...,
'user_uid': ...,
'user_gid': ...,
'user_comment': ...,
'user_home': ...,
'user_shell': ...
}
# %% About
# - Name: Recap About EtcGroup
# - Difficulty: hard
# - Lines: 11
# - Minutes: 13
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Define function `asgroups()` that converts data:
# - parameter: `data: list[tuple[str,...]`
# - returns: `dict[str,list[str]]`
# 2. The function returns a dictionary with data for each user:
# - the key in the dictionary is the `login` of the user
# - the value in the dictionary is a list of groups the user belongs to
# - example: {'alice': [...], 'bob': [...], 'carol': [...]}
# - skip group without users (empty list)
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj funkcję `asgroups()` która przekształci dane:
# - parametr: `data: list[tuple[str,...]`
# - zwraca: `dict[str,list[str]]`
# 2. Funkcja zwraca słownik z danymi dla każdego użytkownika:
# - kluczem w słowniku jest `login` użytkownika
# - wartością w słowniku jest lista grup, do których należy użytkownik
# - przykład: {'alice': [...], 'bob': [...], 'carol': [...]}
# - pomiń grupę bez użytkowników (pustą listę)
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Example
# >>> asgroups(DATA)
# {'adm': ['sys', 'adm'],
# 'alice': ['users', 'staff'],
# 'bin': ['bin', 'sys'],
# 'bob': ['users', 'staff'],
# 'carol': ['users'],
# 'daemon': ['daemon', 'bin', 'adm'],
# 'dave': ['users'],
# 'eve': ['users', 'staff', 'admins'],
# 'root': ['root', 'daemon', 'bin', 'sys', 'adm'],
# 'sys': ['sys']}
# %% Hints
# - `len()`
# - `list[...]`
# - `... in tuple`
# - `str.splitlines()`
# - `str.isspace()`
# - `str.startswith()`
# - `str.split()`
# - `dict.get(...)`
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 12), \
'Python 3.12+ required'
>>> from pprint import pprint
>>> result = asgroups(DATA)
>>> pprint(result)
{'adm': ['sys', 'adm'],
'alice': ['users', 'staff'],
'bin': ['bin', 'sys'],
'bob': ['users', 'staff'],
'carol': ['users'],
'daemon': ['daemon', 'bin', 'adm'],
'dave': ['users'],
'eve': ['users', 'staff', 'admins'],
'root': ['root', 'daemon', 'bin', 'sys', 'adm'],
'sys': ['sys']}
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
type data = list[tuple[str, str, str, str]]
type result = list[str]
asgroups: Callable[[data], result]
# %% Data
DATA = [
# File: /etc/group
('root', '', '0', 'root'),
('daemon', '', '1', 'root,daemon'),
('bin', '', '2', 'root,bin,daemon'),
('sys', '', '3', 'root,bin,sys,adm'),
('adm', '', '4', 'root,adm,daemon'),
('users', '', '1001', 'alice,bob,carol,dave,eve'),
('staff', '', '1002', 'alice,bob,eve'),
('admins', '', '1003', 'eve'),
('nogroup', '', '65534', ''),
]
# %% Result
def asgroups(records, delimiter=','):
"""
Structure of `/etc/group` file:
| Field | Type | Description |
|----------|------|-----------------------------------------------|
| name | str | group name |
| password | str | 'x' indicates that shadow passwords are used |
| gid | int | group id |
| members | list | comma-separated logins matching `/etc/passwd` |
Example:
- users::1001:alice,bob,carol,dave,eve
- [name]:[password]:[gid]:[members]
"""
return ...
# %% About
# - Name: Recap About EtcShadow
# - Difficulty: hard
# - Lines: 30
# - Minutes: 34
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% English
# 1. Define function `asshadow()` that converts data:
# - parameter: `data: list[tuple[str,...]]`
# - returns: `dict[str,str | int | None | bool]`
# 2. The function returns a dictionary with data for each user:
# - the key in the dictionary is the `login` of the user
# - the value in the dictionary is a dictionary with user data
# - example: {'alice': {...}, 'bob': {...}, 'carol': {...}}
# 3. The function must:
# - split the password field by `$` to get modifier, algorithm, salt, and hash
# - determine values for fields `account_locked`, `password_algorithm`, `password_salt`, `password_hash`
# - do not convert `password_last_changed` to `date` type, leave as it is
# - do not convert `password_min_age` to `int` type, leave as it is
# - do not convert `password_max_age` to `int` type, leave as it is
# - do not convert `password_warning_period` to `int` type, leave as it is
# - do not convert `password_inactivity_period` to `int` type, leave as it is
# - do not convert `account_expiration_date` to `date` type, leave as it is
# - do not convert `reserved` to `str` type, leave as it is
# 4. Run doctests - all must succeed
# %% Polish
# 1. Zdefiniuj funkcję `asshadow()` która przekształci dane:
# - parametr: `data: list[tuple[str,...]]`
# - zwraca: `dict[str, str | int | None | bool]`
# 2. Funkcja zwraca słownik z danymi dla każdego użytkownika:
# - kluczem w słowniku jest `login` użytkownika
# - wartością w słowniku jest słownik z danymi użytkownika
# - przykład: {'alice': {...}, 'bob': {...}, 'carol': {...}}
# 3. Funkcja musi:
# - podzielić pole hasła po `$` aby uzyskać modyfikator, algorytm, sól i hash
# - ustalić wartości pól `account_locked`, `password_algorithm`, `password_salt`, `password_hash`
# - nie konwertować `password_last_changed` na typ `date`, zostawić jak jest
# - nie konwertować `password_min_age` na typ `int`, zostawić jak jest
# - nie konwertować `password_max_age` na typ `int`, zostawić jak jest
# - nie konwertować `password_warning_period` na typ `int`, zostawić jak jest
# - nie konwertować `password_inactivity_period` na typ `int`, zostawić jak jest
# - nie konwertować `account_expiration_date` na typ `date`, zostawić jak jest
# - nie konwertować `reserved` na typ `str`, zostawić jak jest
# 4. Uruchom doctesty - wszystkie muszą się powieść
# %% References
# [1] shadow(5) - File Formats Manual
# Year: 2017
# Retrieved: 2025-02-14
# URL: https://manpages.debian.org/unstable/passwd/shadow.5.en.html
# %% Example
# >>> asshadow(DATA, 'alice')
# {'alice': {'account_expiration_date': '',
# 'account_locked': False,
# 'login': 'alice',
# 'password_algorithm': 'sha512crypt',
# 'password_hash': 'bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.',
# 'password_inactivity_period': '',
# 'password_last_changed': '10957',
# 'password_max_age': '99999',
# 'password_min_age': '0',
# 'password_salt': '5H0QpwprRiJQR19Y',
# 'password_warning_period': '7',
# 'reserved': ''},
# 'bob': {'account_expiration_date': '',
# 'account_locked': False,
# 'login': 'bob',
# 'password_algorithm': 'sha512crypt',
# 'password_hash': 'tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50',
# 'password_inactivity_period': '',
# 'password_last_changed': '10957',
# 'password_max_age': '99999',
# 'password_min_age': '0',
# 'password_salt': 'P9zn0KwR',
# 'password_warning_period': '7',
# 'reserved': ''},
# 'carol': {'account_expiration_date': '',
# 'account_locked': True,
# 'login': 'carol',
# 'password_algorithm': 'md5crypt',
# 'password_hash': 'SWlkjRWexrXYgc98F.',
# 'password_inactivity_period': '',
# 'password_last_changed': '10957',
# 'password_max_age': '99999',
# 'password_min_age': '0',
# 'password_salt': '.QKDPc5E',
# 'password_warning_period': '7',
# 'reserved': ''},
# ...}
# %% Hints
# - `len()`
# - `list[...]`
# - `... in tuple`
# - `str.splitlines()`
# - `str.isspace()`
# - `str.startswith()`
# - `str.split()`
# - `dict.get(...)`
# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 12), \
'Python 3.12+ required'
>>> from pprint import pprint
>>> result = asshadow(DATA)
>>> pprint(result, sort_dicts=True)
{'alice': {'account_expiration_date': '',
'account_locked': False,
'login': 'alice',
'password_algorithm': 'sha512crypt',
'password_hash': 'bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.',
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': '5H0QpwprRiJQR19Y',
'password_warning_period': '7',
'reserved': ''},
'bin': {'account_expiration_date': '',
'account_locked': True,
'login': 'bin',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': None,
'password_warning_period': '7',
'reserved': ''},
'bob': {'account_expiration_date': '',
'account_locked': False,
'login': 'bob',
'password_algorithm': 'sha512crypt',
'password_hash': 'tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50',
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': 'P9zn0KwR',
'password_warning_period': '7',
'reserved': ''},
'carol': {'account_expiration_date': '',
'account_locked': True,
'login': 'carol',
'password_algorithm': 'md5crypt',
'password_hash': 'SWlkjRWexrXYgc98F.',
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': '.QKDPc5E',
'password_warning_period': '7',
'reserved': ''},
'daemon': {'account_expiration_date': '',
'account_locked': True,
'login': 'daemon',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': None,
'password_warning_period': '7',
'reserved': ''},
'dave': {'account_expiration_date': '',
'account_locked': True,
'login': 'dave',
'password_algorithm': 'sha512crypt',
'password_hash': 'MxaKfj3Z8F9G8wKz7LU0',
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': 'wXtY9ZoG',
'password_warning_period': '7',
'reserved': ''},
'eve': {'account_expiration_date': '10988',
'account_locked': True,
'login': 'eve',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '30',
'password_last_changed': '10957',
'password_max_age': '90',
'password_min_age': '0',
'password_salt': None,
'password_warning_period': '5',
'reserved': ''},
'mallory': {'account_expiration_date': '',
'account_locked': False,
'login': 'mallory',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '',
'password_last_changed': '',
'password_max_age': '',
'password_min_age': '',
'password_salt': None,
'password_warning_period': '',
'reserved': ''},
'nobody': {'account_expiration_date': '',
'account_locked': True,
'login': 'nobody',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': None,
'password_warning_period': '7',
'reserved': ''},
'root': {'account_expiration_date': '',
'account_locked': True,
'login': 'root',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': None,
'password_warning_period': '7',
'reserved': ''},
'sys': {'account_expiration_date': '',
'account_locked': True,
'login': 'sys',
'password_algorithm': None,
'password_hash': None,
'password_inactivity_period': '',
'password_last_changed': '10957',
'password_max_age': '99999',
'password_min_age': '0',
'password_salt': None,
'password_warning_period': '7',
'reserved': ''}}
"""
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -f -v myfile.py`
# %% Imports
# %% Types
from typing import Callable
type data = list[tuple[str, str, str, str, str, str, str, str, str]]
type result = dict[str, str | int | None | bool]
asshadow: Callable[[data], result]
# %% Data
DATA = [
# File: /etc/shadow
('root', '*', '10957', '0', '99999', '7', '', '', ''),
('daemon', '*', '10957', '0', '99999', '7', '', '', ''),
('bin', '*', '10957', '0', '99999', '7', '', '', ''),
('sys', '*', '10957', '0', '99999', '7', '', '', ''),
('alice', '$6$5H0QpwprRiJQR19Y$bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.', '10957', '0', '99999', '7', '', '', ''),
('bob', '$6$P9zn0KwR$tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50', '10957', '0', '99999', '7', '', '', ''),
('carol', '!$1$.QKDPc5E$SWlkjRWexrXYgc98F.', '10957', '0', '99999', '7', '', '', ''),
('dave', '!$6$wXtY9ZoG$MxaKfj3Z8F9G8wKz7LU0', '10957', '0', '99999', '7', '', '', ''),
('eve', '!!', '10957', '0', '90', '5', '30', '10988', ''),
('mallory', ' ', '', '', '', '', '', '', ''),
('nobody', '*', '10957', '0', '99999', '7', '', '', ''),
]
ALGORITHMS = {
'1': 'md5crypt',
'2a': 'bcrypt (Blowfish)',
'2b': 'bcrypt (Blowfish)',
'2x': 'bcrypt (Blowfish)',
'2y': 'bcrypt (Blowfish)',
'3': 'NTHASH',
'5': 'sha256crypt',
'6': 'sha512crypt',
'7': 'scrypt',
'y': 'yescrypt',
'8': 'PBKDF2',
'gy': 'gost-yescrypt',
'md5': 'Solaris MD5',
'sha1': 'PBKDF1 with SHA-1',
'': 'DES',
}
# %% Result
def asshadow(data):
"""
| Field | Type | Description |
|-------------|--------------|-----------------------------------------------------------------|
| login | str | login name, matching `/etc/passwd` |
| password | str | encrypted password (see below for more details) |
| last_change | date or None | days since 1970-01-01 when the password was changed |
| min_age | int or None | days before which password may not be changed |
| max_age | int or None | days after which password must be changed |
| warning | int or None | days before `max_age` to warn the user to change their password |
| inactive | int or None | days after password expires that account is disabled |
| expiration | date or None | days since 1970-01-01 when account will be disabled |
| reserved | str or None | this field is reserved for future use |
Example:
- alice:$6$wXtY9ZoG$MzaxvKfj3Z8F9G8wKz7LU0:18736:0:99999:7:::
- [login]:[password]:[last_change]:[min_age]:[max_age]:[warning]:[inactive]:[expiration]:[reserved]
Password field (split by `$`):
- modifier
- algorithm
- salt
- password hash
Password algorithms [3]:
- '1' - md5crypt
- '2a' - bcrypt (Blowfish)
- '2b' - bcrypt (Blowfish)
- '2x' - bcrypt (Blowfish)
- '2y' - bcrypt (Blowfish)
- '3' - NTHASH
- '5' - sha256crypt
- '6' - sha512crypt
- '7' - scrypt
- 'y' - yescrypt
- '8' - PBKDF2 with different implementations
- 'gy' - gost-yescrypt
- 'md5' - Solaris MD5
- 'sha1' - PBKDF1 with SHA-1
- none of the above - DES
Password modifiers:
| Modifier | Locked | Algorithm | Salt | Hash | Comment |
|----------|--------|-----------|------|------|-----------------------------------------------------------|
| ' ' | False | None | None | None | password not required to log in |
| '*' | True | None | None | None | password authentication disabled, can use `su` or SSH-key |
| '!!' | True | None | None | None | account inactivated by admin |
| '!' | True | yes | yes | yes | password authentication disabled, can use `su` or SSH-key |
| '' | False | yes | yes | yes | normal account |
Example:
- !$6$wXtY9ZoG$MzaxvKfj3Z8F9G8wKz7LU0
- [modifier]$[algorithm]$[salt]$[hash]
- modifier: !
- locked: True
- algorithm: sha512crypt
- salt: wXtY9ZoG
- hash: MzaxvKfj3Z8F9G8wKz7LU0
"""
return {
'login': ...,
'password_algorithm': ...,
'password_salt': ...,
'password_hash': ...,
'password_last_changed': ...,
'password_min_age': ...,
'password_max_age': ...,
'password_warning_period': ...,
'password_inactivity_period': ...,
'account_expiration_date': ...,
'reserved': ...,
'account_locked': ...,
}