19.1. Recap About

19.1.1. Assignments

# %% About
# - Name: Recap About Calculator
# - Difficulty: easy
# - Lines: 13
# - 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 class `Calculator` with methods:
#    - `__init__()` takes two arguments `a` and `b` and sets them as fields
#    - `add()` which returns sum of `a` and `b`
#    - `sub()` which returns difference of `a` and `b`
#    - `mul()` which returns product of `a` and `b`
#    - `div()` which returns division of `a` and `b`
#    - `mean()` which returns arithemetic average of `a` and `b`
# 3. Run doctests - all must succeed

# %% Polish
# 1. Zdefiniuj klasę `Calculator` z metodami:
#    - `__init__()` przyjmuje dwa argumenty `a` i `b` i ustawia je jako pola
#    - `add()` która zwraca sumę `a` i `b`
#    - `sub()` która zwraca różnicę `a` i `b`
#    - `mul()` która zwraca iloczyn `a` i `b`
#    - `div()` która zwraca iloraz `a` i `b`
#    - `mean()` która zwraca średnią arytmetyczną `a` i `b`
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from inspect import isclass, ismethod

>>> assert isclass(Calculator)
>>> calc = Calculator(1, 2)

>>> assert ismethod(calc.add)
>>> assert ismethod(calc.sub)
>>> assert ismethod(calc.mul)
>>> assert ismethod(calc.div)
>>> assert ismethod(calc.mean)

>>> calc.add()
3
>>> calc.sub()
-1
>>> calc.mul()
2
>>> calc.div()
0.5
>>> calc.mean()
1.5
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports

# %% Types
from typing import Callable
Calculator: type
__init__: Callable[[object, int|float, int|float], None]
add: Callable[[object], int|float]
sub: Callable[[object], int|float]
mul: Callable[[object], int|float]
div: Callable[[object], int|float]
mean: Callable[[object], int|float]

# %% Data

# %% Result
class Calculator:
    ...

# %% About
# - Name: Recap About EtcPasswdUsername
# - Difficulty: medium
# - 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. Create function `get_username`:
#    - parameter: `user_id`
#    - return: `username`
# 2. Return `None` if user does not exist
# 3. Run doctests - all must succeed

# %% Polish
# 1. Stwórz funkcję `get_username`:
#    - parametr: `user_id`
#    - zwraca: `username`
# 2. Zwróć `None` jeżeli użytkownik nie istnieje
# 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
# >>> get_username(0)
# 'root'
#
# >>> get_username(1000)
# 'mwatney'
#
# >>> get_username(999)
# None

# %% Doctests
"""
>>> get_username(0)
'root'

>>> get_username(1000)
'mwatney'

>>> assert get_username(999) is None, \
'Return `None` if user does not exist'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports

# %% Types
from typing import Callable
get_username: Callable[[int], str|None]

# %% Data
# Structure of `/etc/passwd` file [1]:
#
# | 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:
# - mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash
# - [login]:[password]:[uid]:[gid]:[comment]:[home]:[shell]
#
DATA = """root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
nobody:x:99:99:Nobody:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash
mlewis:x:1001:1001:Melissa Lewis:/home/mlewis:/bin/bash
rmartinez:x:1002:1002:Rick Martinez:/home/rmartinez:/bin/bash
avogel:x:1003:1003:Alex Vogel:/home/avogel:/bin/bash
bjohanssen:x:1004:1004:Beth Johanssen:/home/bjohanssen:/bin/bash
cbeck:x:1005:1005:Chris Beck:/home/cbeck:/bin/bash
"""

# %% Result
def get_username(user_id):
    return ...

# %% About
# - Name: Recap About EtcPasswd
# - Difficulty: medium
# - Lines: 15
# - 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. Create function `parse_passwd`:
#    - parameter: `username: str`
#    - return: dict with user data record
# 2. Split line by a colon (`:`):
#    - example: `mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash`
#    - `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
# 2. Return `None` if user does not exist
# 3. Run doctests - all must succeed

# %% Polish
# 1. Stwórz funkcję `parse_passwd`:
#    - parametr: `username: str`
#    - zwraca: dict z danymi użytkownika
# 2. Split line by a colon (`:`):
#    - przykład: `mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash`
#    - `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
# 2. Zwróć `None` jeżeli użytkownik nie istnieje
# 3. Uruchom doctesty - wszystkie muszą się powieść

# %% Example
# >>> parse_passwd('mwatney')
# {'user_login': 'mwatney',
#  'user_password': 'x',
#  'user_uid': 1000,
#  'user_gid': 1000,
#  'user_comment': 'Mark Watney',
#  'user_home': '/home/mwatney',
#  'user_shell': '/bin/bash'}
#
# >>> parse_passwd('notexisting')
# None

# %% Hints
# - `len()`
# - `int()`
# - `str.splitlines()`
# - `str.isspace()`
# - `str.startswith()`
# - `str.split()`

# %% Doctests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> pprint(parse_passwd('mwatney'), sort_dicts=False)
{'user_login': 'mwatney',
 'user_password': 'x',
 'user_uid': 1000,
 'user_gid': 1000,
 'user_comment': 'Mark Watney',
 'user_home': '/home/mwatney',
 'user_shell': '/bin/bash'}

>>> assert parse_passwd('notexisting') is None, \
'Return `None` if user does not exist'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports

# %% Types
from typing import Callable
parse_passwd: Callable[[int], str|None]

# %% Data
# Structure of `/etc/passwd` file [1]:
#
# | 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:
# - mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash
# - [login]:[password]:[uid]:[gid]:[comment]:[home]:[shell]
#
DATA = """root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
nobody:x:99:99:Nobody:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
mwatney:x:1000:1000:Mark Watney:/home/mwatney:/bin/bash
mlewis:x:1001:1001:Melissa Lewis:/home/mlewis:/bin/bash
rmartinez:x:1002:1002:Rick Martinez:/home/rmartinez:/bin/bash
avogel:x:1003:1003:Alex Vogel:/home/avogel:/bin/bash
bjohanssen:x:1004:1004:Beth Johanssen:/home/bjohanssen:/bin/bash
cbeck:x:1005:1005:Chris Beck:/home/cbeck:/bin/bash
"""

# %% Result
def parse_passwd(username):
    return {
        'user_login': ...,
        'user_password': ...,
        'user_uid': ...,
        'user_gid': ...,
        'user_comment': ...,
        'user_home': ...,
        'user_shell': ...
    }

# %% About
# - Name: Recap About EtcGroup
# - Difficulty: medium
# - Lines: 18
# - Minutes: 21

# %% 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. Create function `parse_groups`:
#    - parameter: `username: str`
#    - return: list[str] with user groups
# 2. Split line by a colon (`:`):
#    - example: `user::1001:mwatney,mlewis,rmartinez`
#    - `group_name: str` - group name
#    - `group_password: str` - 'x' indicates that shadow passwords are used
#    - `group_gid: int` - group number
#    - `group_members: str` - comma-separated logins
# 3. Return empty list `[]` if user does not exist
# 4. Run doctests - all must succeed

# %% Polish
# 1. Stwórz funkcję `parse_groups`:
#    - parametr: `username: str`
#    - zwraca: list[str] z grupami użytkownika
# 2. Podziel line po dwukropku (`:`):
#    - przykład: `user::1001:mwatney,mlewis,rmartinez`
#    - `group_name: str` - nazwa grupy
#    - `group_password: str` - 'x' mówi, że hasło jest w pliku shadow
#    - `group_gid: int` - numer grupy
#    - `group_members: str` - loginy oddzielone przecinkami
# 3. Zwróć pustą listę `[]` jeżeli użytkownik nie istnieje
# 4. Uruchom doctesty - wszystkie muszą się powieść

# %% Example
# >>> parse_groups('mwatney')
# ['user', 'staff']
#
# >>> parse_groups('root')
# ['root', 'bin', 'sys', 'adm', 'mail', 'daemon', 'sysadmin']
#
# >>> parse_groups('notexisting')
# []

# %% 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, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> result = parse_groups('mwatney')
>>> pprint(result)
['user', 'staff']

>>> result = parse_groups('root')
>>> pprint(result)
['root', 'bin', 'sys', 'adm', 'mail', 'daemon', 'sysadmin']

>>> assert parse_groups('notexisting') == [], \
'Return `[]` if user does not exist'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports

# %% Types
from typing import Callable
parse_groups: Callable[[str,...], list[str]]

# %% Data
# 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:
# - admin::1003:mlewis,bjohanssen
# - [name]:[password]:[gid]:[members]
#
DATA = """root::0:root
other::1:
bin::2:root,bin,daemon
sys::3:root,bin,sys,adm
adm::4:root,adm,daemon
mail::6:root
daemon::12:root,daemon
sysadmin::14:root
user::1001:mwatney,mlewis,rmartinez,avogel,bjohanssen,cbeck
staff::1002:mwatney,mlewis,rmartinez,bjohanssen,cbeck
admin::1003:mlewis,bjohanssen
nobody::60001:
noaccess::60002:
nogroup::65534:
"""

# %% Result
def parse_groups(username):
    return []

# %% About
# - Name: Recap About EtcShadow
# - Difficulty: medium
# - Lines: 35
# - 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. Create function `parse_shadow`:
#    - parameter: `username: str`
#    - return: user password record as a dict
# 2. Split password field by `$`:
#    - algorithm (algorithm number, e.g. `6` is SHA-512)
#    - salt (value added to password)
#    - password hash (encoded password)
# 3. Return `None` if user does not exist
# 4. Run doctests - all must succeed

# %% Polish
# 1. Stwórz funkcję `parse_shadow`:
#    - parametr: `username: str`
#    - zwraca: dane hasła użytkownika jako dict
# 2. Podziel pole hasła po znaku `$`:
#    - algorytm (nr algorytmu, np. `6` to SHA-512)
#    - sól (wartość dodawana do hasła)
#    - hash hasła (zakodowane hasło)
# 3. Zwróć `None` jeżeli użytkownik nie istnieje
# 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
# >>> parse_shadow('mwatney')
# {'account_expiration_date': '',
#  'account_locked': False,
#  'login': 'mwatney',
#  'password_algorithm': 'SHA-512',
#  'password_hash': 'bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.',
#  'password_inactivity_period': '',
#  'password_last_changed': '16550',
#  'password_max_age': '',
#  'password_min_age': '',
#  'password_salt': '5H0QpwprRiJQR19Y',
#  'password_warning_period': '',
#  'reserved': ''}
#
# >>> parse_shadow('mlewis')
# {'account_expiration_date': '',
#  'account_locked': False,
#  'login': 'mlewis',
#  'password_algorithm': 'SHA-512',
#  'password_hash': 'tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50',
#  'password_inactivity_period': '',
#  'password_last_changed': '16632',
#  'password_max_age': '99999',
#  'password_min_age': '0',
#  'password_salt': 'P9zn0KwR',
#  'password_warning_period': '7',
#  'reserved': ''}
#
# >>> parse_shadow('notexisting')
# None

# %% 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, 9), \
'Python 3.9+ required'

>>> from pprint import pprint

>>> result = parse_shadow('mwatney')
>>> pprint(result, sort_dicts=True)
{'account_expiration_date': '',
 'account_locked': False,
 'login': 'mwatney',
 'password_algorithm': 'sha512crypt',
 'password_hash': 'bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.',
 'password_inactivity_period': '',
 'password_last_changed': '16550',
 'password_max_age': '',
 'password_min_age': '',
 'password_salt': '5H0QpwprRiJQR19Y',
 'password_warning_period': '',
 'reserved': ''}

>>> result = parse_shadow('mlewis')
>>> pprint(result, sort_dicts=True)
{'account_expiration_date': '',
 'account_locked': False,
 'login': 'mlewis',
 'password_algorithm': 'sha512crypt',
 'password_hash': 'tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50',
 'password_inactivity_period': '',
 'password_last_changed': '16632',
 'password_max_age': '99999',
 'password_min_age': '0',
 'password_salt': 'P9zn0KwR',
 'password_warning_period': '7',
 'reserved': ''}

>>> assert parse_shadow('notexisting') is None, \
'Return `None` if user does not exist'
"""

# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`

# %% Imports

# %% Types
from typing import Callable
parse_shadow: Callable[[str,...], dict|None]

# %% Data
# Structure of `/etc/shadow` file [1]:
#
# | Field       | Type | Description                                                     |
# |-------------|------|-----------------------------------------------------------------|
# | login       | str  | login name, matching `/etc/passwd`                              |
# | password    | str  | encrypted password (see below for more details)                 |
# | last_change | date | days since 1970-01-01 when the password was changed             |
# | min_age     | int  | days before which password may not be changed                   |
# | max_age     | int  | days after which password must be changed                       |
# | warning     | int  | days before `max_age` to warn the user to change their password |
# | inactive    | int  | days after password expires that account is disabled            |
# | expiration  | date | days since 1970-01-01 when account will be disabled             |
# | reserved    | str  | 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:
#
# | Character     | Locked | Algorithm | Password | Salt | 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                              |
# | '!<password>' | True   | yes       | yes      | yes  | password authentication disabled, can use `su` or SSH-key |
# | '<password>'  | False  | yes       | yes      | yes  | normal account                                            |
#
# Example:
# - !$6$wXtY9ZoG$MzaxvKfj3Z8F9G8wKz7LU0
# - [modifier]$[algorithm]$[salt]$[hash]
# - modifier: !
# - locked: True
# - algorithm: sha512crypt
# - salt: wXtY9ZoG
# - hash: MzaxvKfj3Z8F9G8wKz7LU0
#
DATA = """root:$6$Ke02nYgo.9v0SF4p$hjztYvo/M4buqO4oBX8KZTftjCn6fE4cV5o/I95QPekeQpITwFTRbDUBYBLIUx2mhorQoj9bLN8v.w6btE9xy1:16431:0:99999:7:::
bin:*:16431:0:99999:7:::
daemon:*:16431:0:99999:7:::
adm:*:16431:0:99999:7:::
shutdown:*:16431:0:99999:7:::
halt:*:16431:0:99999:7:::
nobody:*:16431:0:99999:7:::
sshd:*:16431:0:99999:7:::
mwatney:$6$5H0QpwprRiJQR19Y$bXGOh7dIfOWpUb/Tuqr7yQVCqL3UkrJns9.7msfvMg4ZO/PsFC5Tbt32PXAw9qRFEBs1254aLimFeNM8YsYOv.:16550::::::
mlewis:$6$P9zn0KwR$tgfvvFWJJ5FKmoXiP5rXWOjwoEBOEoAuBi3EphRbJqqjWYvhEM2wa67L9XgQ7W591FxUNklkDIQsk4kijuhE50:16632:0:99999:7:::
rmartinez:$1$.QKDPc5E$SWlkjRWexrXYgc98F.:12825:0:90:5:30:13096:
avogel:!!:16431:0:99999:7:::
bjohanssen:$6$wXtY9ZoG$MxaKfj3Z8F9G8wKz7LU0:16431:0:99999:7:::
cbeck:*:16431: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 parse_shadow(username):
    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': ...,
    }