Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • sgraupner/ds_cs4bd_2324
  • akbi5459/ds_cs4bd_2324
  • s90907/ds_cs4bd_2324
  • fapr2511/ds_cs4bd_2324
4 results
Show changes
Commits on Source (20)
Showing
with 1777 additions and 148 deletions
......@@ -3,7 +3,7 @@
"python.testing.unittestArgs": [
"-v",
"-s",
"./C_expressions",
".",
"-p",
"test_*.py"
],
......
......@@ -68,7 +68,7 @@ Perform examples and answer questions on a piece of paper.
>>> fruitbox = ('apple', 'pear', 'orange', 'banana', 'apple')
>>> print(fruits)
['apple', 'pear', 'orange', 'banana']
['apple', 'pear', 'orange', 'banana', 'apple']
>>> print(fruitbox)
('apple', 'pear', 'orange', 'banana', 'apple')
......@@ -85,15 +85,24 @@ Perform examples and answer questions on a piece of paper.
>>>
```
1. How is the structure for Eric called? (1 Pt)
1. How is the structure for `eric1` called? What is the difference to
`eric2`? Explain outputs. (1 Pt)
```py
eric = {"name": "Eric", "salary": 5000, "birthday": "Sep 25 2001"}
eric1 = {"name": "Eric", "salary": 5000, "birthday": "Sep 25 2001"}
eric2 = {"name", "Eric", "salary", 5000, "birthday", "Sep 25 2001"}
>>> print(eric)
>>> print(eric1)
{'name': 'Eric', 'salary': 5000, 'birthday': 'Sep 25 2001'}
>>> print(eric["salary"])
>>> print(eric2)
{'Sep 25 2001', 5000, 'name', 'Eric', 'birthday', 'salary'}
#
# print(eric2) in same order?
# print salary for eric1 and eric2?
>>> print(eric1["salary"])
5000
```
......
{
"python.testing.unittestArgs": [
"-v",
"-s",
".",
"-p",
"test_*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
\ No newline at end of file
......@@ -7,7 +7,7 @@ This assignment demonstrates Python's powerful (*"one-liner"*) expressions.
- [Challenge 2:](#2-challenge-run-code) Run Code
- [Challenge 3:](#3-challenge-run-unit-tests) Run Unit Tests
- [Challenge 4:](#4-challenge-write-expressions) Write Expressions
- [Challenge 5:](#5-challenge-final-test) Final Test
- [Challenge 5:](#5-challenge-final-test-and-sign-off) Final Test and sign-off
Points: [1, 2, 3, 0, 10]
......@@ -67,17 +67,18 @@ pwd # print working directory
python expressions.py # run program
-->
numbers: [4, 12, 3, 8, 17, 12, 1, 8, 7]
#
a) number of numbers: 9
b) first three numbers: []
c) last three numbers: []
d) last three numbers reverse: []
e) odd numbers: []
f) number of odd numbers: 0
g) sum of odd numbers: 0
h) duplicate numbers removed: []
i) number of duplicate numbers: 0
j) ascending, de-dup (n^2) numbers: []
k) length: NEITHER
b) first three numbers: [4, 12, 3]
c) last three numbers: [1, 8, 7]
d) last three numbers reverse: [7, 8, 1]
e) odd numbers: [3, 17, 1, 7]
f) number of odd numbers: 4
g) sum of odd numbers: 28
h) duplicate numbers removed: [1, 3, 4, 7, 8, 12, 17]
i) number of duplicate numbers: 2
j) ascending, de-dup (n^2) numbers: [1, 9, 16, 49, 64, 144, 289]
k) length: ODD_LIST
```
(1 Pt)
......@@ -261,24 +262,34 @@ Continue until all tests pass.
 
### 5.) Challenge: Final Test
### 5.) Challenge: Final Test and sign-off
For sign-off, change into `C_expressions` directory and copy commands into a terminal:
Pull latest Unit-test file from GitLab and re-run tests:
```sh
wget https://gitlab.bht-berlin.de/sgraupner/ds_cs4bd_2324/-/blob/main/C_express
ions/test_expressions.py
# Fetch test file from Gitlab and run tests for sign-off.
# The sed-command removes comments from test cases.
python test_expressions.py --tests=all
test_url=https://gitlab.bht-berlin.de/sgraupner/ds_cs4bd_2324/-/raw/main/C_expressions/test_expressions.py
curl $test_url | \
sed -e 's/^#.*Test_case_/Test_case_/' | \
python
```
Result:
```
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 7874 100 7874 0 0 55666 0 --:--:-- --:--:-- --:--:-- 56242
...........
----------------------------------------------------------------------
Ran 11 tests in 0.002s
Ran 11 tests in 0.003s
OK
<unittest.runner.TextTestResult run=11 errors=0 failures=0>
```
11 tests succeeded.
(10 Pts, 1 Pt for each test passing)
import os
"""
Special file __init__.py marks a directory as a Python package.
A Python Package is a collection of Python modules with an
__init__.py File. The file is executed once when any .py file
from the directory is imported for the first time.
Special file __init__.py marks a directory as a Python module.
The file is executed once when any .py file is imported.
//
Python unittest require the presence of (even an empty) file.
"""
def package_dir(file):
"""
Return name of directory of this package.
"""
path = os.path.normpath(file).split(os.sep)
return path[len(path)-2] # e.g. "C_expressions"
def project_path(file):
"""
Return path to project directory.
"""
path = os.path.normpath(file).split(os.sep)
return os.path.dirname(file)[:-len(PACKAGE_DIR)-1]
def import_sol_module(file):
"""
Import and return module with name "file + '_sol'".
Raises ImportError exception, if _sol file does not exist.
"""
sol_module = (file.split("\\")[-1:])[0].split(".")[0] + "_sol"
return __import__(sol_module, globals(), locals(), [], 0)
# name of this package directory
PACKAGE_DIR = package_dir(__file__)
# path to project directory, in which this module resides
PROJECT_PATH = project_path(__file__)
# load setup module when executed in parent directory
try:
__import__('setup')
#
except ImportError:
pass
from __init__ import import_sol_module
class Expressions:
""""
Class for the assignment. Fill in one-line expressions (no own functions)
to initialize values self.b .. self.k with specified values.
Fill in one-line expressions (no own functions) to initialize attributes
self.b .. self.k with specified values.
Use Python built-in functions, list expressions and list comprehension,
but NOT own functions.
Complete tasks one after another. Once you are done with one task,
uncomment test case in test_expressions.py. Remove comments for
uncomment test cases in test_expressions.py. Remove comments for
# Test_case_b = Test_case
# Test_case_c = Test_case
# Test_case_d = Test_case
# ...
Run tests in IDE and in a terminal:
python test_expressions.py
python -m unittest test_expressions.py
python -m unittest
"""
default_numbers=[4, 12, 3, 8, 17, 12, 1, 8, 7]
......@@ -63,9 +61,9 @@ class Expressions:
# attempt to load solution module (ignore)
try:
mod = import_sol_module(__file__)
mod.set_solution(self) # replace empty values with solutions
# print(f'solution module found: {solution_module}.py')
_from, _import = 'expressions_sol', 'Stream'
mod = __import__(_from, fromlist=[_import])
mod.set_solution(self) # invoke set_solution() to replace values with solutions
#
except ImportError:
pass
......@@ -94,11 +92,12 @@ class Expressions:
if __name__ == '__main__':
'''
Driver that runs when this file is directly executed.
Driver code that runs when this file is directly executed.
'''
#
n1 = Expressions() # use default list
n1 = Expressions() # use default list
#
# 2nd object with different list
n2 = Expressions([1, 4, 6, 67, 6, 8, 23, 8, 34, 49, 67,
6, 8, 23, 37, 67, 6, 34, 19, 67, 6, 8])
#
......
......@@ -12,26 +12,21 @@ OK
<unittest.runner.TextTestResult run=11 errors=0 failures=0>
"""
import unittest
import sys, getopt
import abc # import Abstract Base Class (ABC) from abc
import expressions
from expressions import Expressions as Tested_class
from __init__ import PACKAGE_DIR, PROJECT_PATH, import_sol_module
import abc # import Abstract Base Class (ABC) from module abc
from expressions import Expressions
class Test_data:
"""
Class with tested objects (objects "under test", "ut") as
instances of imported Tested_class
"""
ut1 = Tested_class(Tested_class.default_numbers) # [4, 12, 3, 8, 17, 12, 1, 8, 7]
ut2 = Tested_class([1, 4, 6, 67, 6, 8, 23, 8, 34, 49, 67, 6, 8, 23, 37, 67, 6, 34, 19, 67, 6, 8])
ut3 = Tested_class([6, 67, 6, 8, 17, 3, 6, 8])
ut4 = Tested_class([8, 3, 9])
ut5 = Tested_class([1, 1, 1])
ut6 = Tested_class([0, 0])
ut7 = Tested_class([0])
ut8 = Tested_class([])
"""
tested objects (objects "under test", "ut") as instances of the Expressions class
"""
ut1 = Expressions(Expressions.default_numbers) # [4, 12, 3, 8, 17, 12, 1, 8, 7]
ut2 = Expressions([1, 4, 6, 67, 6, 8, 23, 8, 34, 49, 67, 6, 8, 23, 37, 67, 6, 34, 19, 67, 6, 8])
ut3 = Expressions([6, 67, 6, 8, 17, 3, 6, 8])
ut4 = Expressions([8, 3, 9])
ut5 = Expressions([1, 1, 1])
ut6 = Expressions([0, 0])
ut7 = Expressions([0])
ut8 = Expressions([])
class Test_case(unittest.TestCase):
......@@ -41,35 +36,26 @@ class Test_case(unittest.TestCase):
Sub-classes are discovered as unit tests.
"""
def setUp(self):
self.ut1 = Test_data.ut1
self.ut2 = Test_data.ut2
self.ut3 = Test_data.ut3
self.ut4 = Test_data.ut4
self.ut5 = Test_data.ut5
self.ut6 = Test_data.ut6
self.ut7 = Test_data.ut7
self.ut8 = Test_data.ut8
# disable tests by assigning Python's Abstract Base Class (ABC)
DefaultTestClass = abc.ABC
# https://www.tutorialspoint.com/python/python_command_line_arguments.htm
argv = sys.argv[1:]
opts, args = getopt.getopt(argv, "", ["tests="])
for opt, arg in opts:
if opt=='-t':
print(f'-t: {arg}')
if opt=='--tests' and arg=="all":
DefaultTestClass = Test_case
self.ut1 = ut1
self.ut2 = ut2
self.ut3 = ut3
self.ut4 = ut4
self.ut5 = ut5
self.ut6 = ut6
self.ut7 = ut7
self.ut8 = ut8
# disable tests by assigning Python's Abstract Base Class (ABC) to test
# case classes, which will not be discovered as unit tests
Test_case_a = Test_case_b = Test_case_c = Test_case_d = \
Test_case_e = Test_case_f = Test_case_g = Test_case_h = \
Test_case_i = Test_case_j = Test_case_k = DefaultTestClass
Test_case_i = Test_case_j = Test_case_k = abc.ABC
# uncomment tests one after another as you progress with
# expressions b) through k)
# assign Test_case class (above) as subclass of unittest.TestCase and with
# attributes of tested objects (self.ut1...ut8)
# uncomment tests one after another as you progress with expressions
Test_case_a = Test_case # test a) passes, solution is given in numbers.py
# Test_case_b = Test_case
# Test_case_c = Test_case
......@@ -141,7 +127,7 @@ class TestCase_d_last_threeClass_in_reverse(Test_case_d):
class TestCase_e_odd_numbers(Test_case_e):
#
# tests e): odd numbers
# tests e): odd numbers, order must be preserved
def test_e_odd_numbers(td):
td.assertEqual(td.ut1.e, [3, 17, 1, 7])
td.assertEqual(td.ut2.e, [1, 67, 23, 49, 67, 23, 37, 67, 19, 67])
......@@ -183,12 +169,12 @@ class TestCase_g_sum_of_odd_numbers(Test_case_g):
class TestCase_h_duplicateClass_removed(Test_case_h):
#
# tests h): duplicate numbers removed
# tests h): duplicate numbers removed - use set() to accept any order
def test_h_duplicateClass_removed(td):
td.assertEqual(td.ut1.h, [4, 12, 3, 8, 17, 1, 7])
td.assertEqual(td.ut2.h, [1, 4, 6, 67, 8, 23, 34, 49, 37, 19])
td.assertEqual(td.ut3.h, [6, 67, 8, 17, 3])
td.assertEqual(td.ut4.h, [8, 3, 9])
td.assertEqual(set(td.ut1.h), {4, 12, 3, 8, 17, 1, 7})
td.assertEqual(set(td.ut2.h), {1, 4, 6, 67, 8, 23, 34, 49, 37, 19})
td.assertEqual(set(td.ut3.h), {6, 67, 8, 17, 3})
td.assertEqual(set(td.ut4.h), {8, 3, 9})
td.assertEqual(td.ut5.h, [1])
td.assertEqual(td.ut6.h, [0])
td.assertEqual(td.ut7.h, [0])
......@@ -213,10 +199,10 @@ class TestCase_j_ascending_squaredClass_no_duplicates(Test_case_j):
#
# tests j): ascending list of squared numbers with no duplicates
def test_j_ascending_squaredClass_no_duplicates(td):
td.assertEqual(td.ut1.j, [1, 9, 16, 49, 64, 144, 289])
td.assertEqual(td.ut2.j, [1, 16, 36, 64, 361, 529, 1156, 1369, 2401, 4489])
td.assertEqual(td.ut3.j, [9, 36, 64, 289, 4489])
td.assertEqual(td.ut4.j, [9, 64, 81])
td.assertEqual(set(td.ut1.j), {1, 9, 16, 49, 64, 144, 289})
td.assertEqual(set(td.ut2.j), {1, 16, 36, 64, 361, 529, 1156, 1369, 2401, 4489})
td.assertEqual(set(td.ut3.j), {9, 36, 64, 289, 4489})
td.assertEqual(set(td.ut4.j), {9, 64, 81})
td.assertEqual(td.ut5.j, [1])
td.assertEqual(td.ut6.j, [0])
td.assertEqual(td.ut7.j, [0])
......@@ -238,13 +224,4 @@ class TestCase_k_classifyClass_as_odd_even_empty(Test_case_k):
if __name__ == '__main__':
#
# discover tests in this package
test_classes = unittest.defaultTestLoader \
.discover(PACKAGE_DIR, pattern='test_*.py', top_level_dir=PROJECT_PATH)
#
verbosity_level = 1
suite = unittest.TestSuite(test_classes)
runner = unittest.runner.TextTestRunner(verbosity=verbosity_level)
result = runner.run(suite)
print(result)
unittest.main()
......@@ -37,7 +37,7 @@ for recursions.
- [Challenge 4:](#4-challenge-powerset) Powerset
- [Challenge 5:](#5-challenge-find-matching-pairs) Find Matching Pairs
- [Challenge 6:](#6-challenge-combinatorial-problem-of-finding-numbers) Combinatorial Problem of Finding Numbers
- [Challenge 7:](#7-challenge-hard-problem-finding-numbers) Hard Problem Finding Numbers
- [Challenge 7:](#7-challenge-hard-problem-of-finding-numbers) Hard Problem of Finding Numbers
Points: [2, 1, 2, 2, 2, 3, 2, +4 extra pts]
......@@ -114,22 +114,28 @@ Italian mathematician *Leonardo of Pisa*, later known as
introduced the sequence to Western European mathematics in his 1202 book
*[Liber Abaci](https://en.wikipedia.org/wiki/Liber_Abaci)*.
![image](../markup/img/fibonacci.jpg)
Numbers of the *Fibonacci sequence* are defined as: *fib(0): 0*, *fib(1): 1*, *...*
and each following number is the sum of the two preceding numbers.
Numbers of the *Fibonacci sequence* are defined as: *fib(0): 0*, *fib(1): 1*, *...* and each following
number is the sum of the two preceding numbers.
Fibonacci numbers are widely found in *nature*, *science*, *social behaviors* of
populations and *arts*, e.g. they form the basis of the
[Golden Ratio](https://www.adobe.com/creativecloud/design/discover/golden-ratio.html),
which is widely used in *painting* and *photography*, see also this
[1:32min](https://www.youtube.com/watch?v=v6PTrc0z4w4) video.
Fibonacci numbers are found in many places in nature, social behavior and arts
([1:32min](https://www.youtube.com/watch?v=v6PTrc0z4w4) video).
<img src="../markup/img/fibonacci.jpg" alt="drawing" width="640"/>
<!-- ![image](../markup/img/fibonacci.jpg) -->
Complete functions `fib(n)` and `fig_gen(n)`.
&nbsp;
Complete functions `fib(n)` and `fib_gen(n)`.
```py
def fib(self, _n) -> int:
# return value of n-th Fibonacci number
return #...
def fib_seq(self, _n):
def fib_gen(self, _n):
# return a generator object that yields two lists, one with n and the
# other with corresponding fib(n)
yield #...
......@@ -424,7 +430,7 @@ Answer questions:
&nbsp;
### 7.) Challenge: Hard Problem Finding Numbers
### 7.) Challenge: Hard Problem of Finding Numbers
Larger data sets can no longer be solved *"brute force"* by exploring all possible
2^n combinations.
......
from functools import cmp_to_key
"""
Assignment_D: recursion
"""
class Recursion:
......@@ -20,7 +21,7 @@ class Recursion:
return 0
def fib(self, _n, memo={}) -> int:
def fib(self, _n, memo=None) -> int:
"""
Return value of n-th Fibonacci number.
- input: n=8
......@@ -30,7 +31,7 @@ class Recursion:
return 0
def fib_seq(self, _n):
def fib_gen(self, _n):
"""
Return a generator object that yields two lists, one with n and the
other with corresponding fib(n).
......@@ -152,7 +153,7 @@ if __name__ == '__main__':
# Challenge 2.1, fig_gen()
if 21 in run_choices:
gen = n1.fib_seq(20) # yield generator object
gen = n1.fib_gen(20) # yield generator object
n, fib = next(gen) # trigger generator
print(f'n: {n}')
print(f'fib(n): {fib}')
......@@ -196,7 +197,7 @@ if __name__ == '__main__':
if 52 in run_choices:
n = 12
pairs = n1.find_pairs(n, lst)
print(f'find_sum({n}, list) -> {pairs}')
print(f'find_pairs({n}, list) -> {pairs}')
lst = [8, 10, 2, 14, 4] # input list
......
{
"python.testing.unittestArgs": [
"-v",
"-s",
".",
"-p",
"test_*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
\ No newline at end of file
# Assignment E: Data Streams in Python &nbsp; (12 Pts)
### Challenges
- [Challenge 1:](#1-challenge-data-streams-in-python) Data Streams in Python
- [Challenge 2:](#2-challenge-map-function) *map()* function
- [Challenge 3:](#3-challenge-reduce-function) *reduce()* function
- [Challenge 4:](#4-challenge-sort-function) *sort()* function
- [Challenge 5:](#5-challenge-pipeline-for-product-codes) Pipeline for Product Codes
- [Challenge 6:](#6-challenge-run-unit-tests) Run Unit Tests
- [Challenge 7:](#7-challenge-sign-off) Sign-off
Points: [1, 2, 3, 3, 2, 0, 1]
&nbsp;
### 1.) Challenge: Data Streams in Python
Data streams are powerful abstractions for data-driven applications that also work in distributed environments. Big Data platforms often build on streams such as
[Spark Streams](https://spark.apache.org/docs/latest/streaming-programming-guide.html) or
[Kafka](https://kafka.apache.org/documentation/streams).
A data stream starts with a *source* (here just a list of names) followed by a pipeline of *chainable operations* performed on each data element passing through the stream. Results can be collected at the *terminus* of the stream.
Pull Python file [stream.py](stream.py).
```py
class Stream:
"""
Class of a data stream comprised of a sequence of stream operations:
"""
class __Stream_op:
"""
Inner class of one stream operation with chainable functions.
Instances comprise the stream pipeline.
"""
def slice(self, i1, i2=None, i3=1):
# function that returns new __Stream_op instance that slices stream
if i2 == None:
i2, i1 = i1, 0
#
return self.__new(self.__data[i1:i2:i3])
def filter(self, filter_func=lambda d : True) ...
# return new __Stream_op instance that passes only elements for
# which filter_func yields True
def map(self, map_func=lambda d : d) ...
# return new __Stream_op instance that passes elements resulting
# from map_func of corresponding elements in the inbound stream
def reduce(self, reduce_func, start=0) -> any: ...
# terminal function that returns single value compounded by reduce_func
def sort(self, comperator_func=lambda d1, d2 : True) ...
# return new __Stream_op instance that passes stream sorted by
# comperator_func
def cond(self, cond: bool, conditional): ...
# return same __Stream_op instance or apply conditional function
# on __Stream_op instance if condition yields True
def print(self) ...
# return same, unchanged __Stream_op instance and print as side effect
def count(self) -> int: ...
# terminal function that returns number of elements in terminal stream
def get(self) -> any: ...
# terminal function that returns final stream __data
```
Application of the stream can demonstrated by the example of a stream of names. The stream is instantiated from the `names` list. The `source()` - method returns the first `__Stream_op` - instance onto which chainable stream methods can be attached.
The stream in the example filters names of lenght = 4, prints those names and counts their number. The *lambda*-expression controls the filter process. Only names of length 4 pass to subsequent pipeline operations.
```py
names = ['Gonzalez', 'Gill', 'Hardin', 'Richardson', 'Buckner', 'Marquez',
'Howe', 'Ray', 'Navarro', 'Talley', 'Bernard', 'Gomez', 'Hamilton',
'Case', 'Petty', 'Lott', 'Casey', 'Hall', 'Pena', 'Witt', 'Joyner',
'Raymond', 'Crane', 'Hendricks', 'Vance', 'Cleveland', 'Duncan', 'Soto',
'Brock', 'Graham', 'Nielsen', 'Rutledge', 'Strong', 'Cox']
result = Stream(names).source() \
.filter(lambda n : len(n) == 4) \
.print() \
.count()
print(f'found {result} names with 4 letters.')
```
Output:
```c++
['Gill', 'Howe', 'Case', 'Lott', 'Hall', 'Pena', 'Witt', 'Soto']
found 8 names with 4 letters.
```
**Questions:**
- How does method chaining work?
- What is required for chainable methods?
- How does a data pipeline gets formed in the example?
- Draw a sketch of data objects and how they are linked from the example above.
(1 Pts)
&nbsp;
### 2.) Challenge: *map()* function
Complete the `map()` function in [stream.py](stream.py) so that the example produces
the desired result: Names are mapped to name lengths for the first 8 names.
Name lengths are then compounded to a single result.
```py
result = Stream(names).source() \
.slice(8) \
.print() \
.map(lambda n : len(n)) \
.print()
```
Output:
```c++
['Gonzalez', 'Gill', 'Hardin', 'Richardson', 'Buckner', 'Marquez', 'Howe', 'Ray']
[8, 4, 6, 10, 7, 7, 4, 3]
```
(2 Pts)
&nbsp;
### 3.) Challenge: *reduce()* function
Complete the `reduce()` function in [stream.py](stream.py) so that name lengths are
compounded (added one after another) to a single result.
```py
result = Stream(names).source() \
.slice(8) \
.print() \
.map(lambda n : len(n)) \
.print() \
.reduce(lambda x, y : x + y)
#
print(f'compound number of letters in names is: {result}.')
```
Output:
```c++
['Gonzalez', 'Gill', 'Hardin', 'Richardson', 'Buckner', 'Marquez', 'Howe', 'Ray']
[8, 4, 6, 10, 7, 7, 4, 3]
compound number of letters in names is: 49.
```
(2 Pts)
3.1) Test your implementation to also work for the next example that produces
a single string of all n-letter names:
```py
n = 5
result = Stream(names).source() \
.filter(lambda name : len(name) == n) \
.print() \
.map(lambda n : n.upper()) \
.reduce(lambda x, y : str(x) + str(y), '')
#
print(f'compounded {n}-letter names: {result}.')
```
Output for n=3 and n=5:
```c++
['Ray', 'Cox']
compounded 3-letter names: RAYCOX.
['Gomez', 'Petty', 'Casey', 'Crane', 'Vance', 'Brock']
compounded 5-letter names: GOMEZPETTYCASEYCRANEVANCEBROCK.
```
(1 Pts)
&nbsp;
### 4.) Challenge: *sort()* function
Complete the `sort()` function in [stream.py](stream.py) so that the example produces
the desired result (use Python's built-in `sort()` or `sorted()` functions).
```py
Stream(names).source() \
.slice(8) \
.print('unsorted: ') \
.sort() \
.print(' sorted: ')
```
Output:
```c++
unsorted: ['Gonzalez', 'Gill', 'Hardin', 'Richardson', 'Buckner', 'Marquez', 'Howe', 'Ray']
sorted: ['Buckner', 'Gill', 'Gonzalez', 'Hardin', 'Howe', 'Marquez', 'Ray', 'Richardson']
```
(1 Pts)
4.1) Understand the sorted sequence below and define a `comperator` (expression that compares two elements (n1, n2) and yields `-1` if n1 should come before n2, `+1` if n1 must be after n2 or `0` if n1 is equal to n2):
```py
len_alpha_comperator = lambda ...
Stream(names).source() \
.sort(len_alpha_comperator) \
.print('sorted: ')
```
Output:
```c++
sorted: ['Cox', 'Ray', 'Case', 'Gill', 'Hall', 'Howe', 'Lott', 'Pena', 'Soto', 'Witt', 'Brock', 'Casey', 'Crane', 'Gomez', 'Petty', 'Vance', 'Duncan', 'Graham', 'Hardin', 'Joyner', 'Strong', 'Talley', 'Bernard', 'Buckner', 'Marquez', 'Navarro', 'Nielsen', 'Raymond', 'Gonzalez', 'Hamilton', 'Rutledge', 'Cleveland', 'Hendricks', 'Richardson']
```
(1 Pts)
4.2) Extend the pipeline so that it produces the following output:
```c++
sorted: [('Cox', 'Xoc', 3), ('Ray', 'Yar', 3), ('Brock', 'Kcorb', 5), ('Casey', 'Yesac', 5), ('Crane', 'Enarc', 5), ('Gomez', 'Zemog', 5), ('Petty', 'Yttep', 5), ('Vance', 'Ecnav', 5), ('Bernard', 'Dranreb', 7), ('Buckner', 'Renkcub', 7), ('Marquez', 'Zeuqram', 7), ('Navarro', 'Orravan', 7), ('Nielsen', 'Neslein', 7), ('Raymond', 'Dnomyar', 7), ('Cleveland', 'Dnalevelc', 9), ('Hendricks', 'Skcirdneh', 9)]
\\
16 odd-length names found.
```
(1 Pts)
&nbsp;
### 5.) Challenge: Pipeline for Product Codes
Build a pipeline that produces batches of five 6-digit numbers with prefix 'X'.
Numbers are in ascending order within each batch and end with a 1-digit checksum
after a dash. The checksum is the sum of all six digits of the random number modulo 10.
```py
for i in range(1, 5):
# Stream of 5 random numbers from integer range, feel free to change
codes = Stream([random.randint(100000,999999) for j in range(5)]).source() \
... \
.get()
#
print(f'batch {i}: {codes}')
```
Output:
```c++
batch 1: ['X102042-9', 'X102180-2', 'X103228-6', 'X104680-9', 'X106782-4']
batch 2: ['X200064-2', 'X200732-4', 'X202090-3', 'X209056-2', 'X211464-8']
batch 3: ['X300186-8', 'X301416-5', 'X305962-5', 'X307938-0', 'X312524-7']
batch 4: ['X400216-3', 'X401436-8', 'X401682-1', 'X405256-2', 'X406376-6']
```
(1 Pts)
5.1) Alter the pipeline such that it produces only even digit codes:
```c++
batch 1: ['X226840-2', 'X284240-0', 'X448288-4', 'X804080-0', 'X888620-2']
batch 2: ['X220640-4', 'X248066-6', 'X648466-4', 'X680404-2', 'X882868-0']
batch 3: ['X262626-4', 'X608662-8', 'X626404-2', 'X662424-4', 'X846228-0']
batch 4: ['X224200-0', 'X282204-8', 'X448426-8', 'X600282-8', 'X802882-8']
```
(1 Pts)
&nbsp;
### 6.) Challenge: Run Unit Tests
Pull file
[test_stream.py](test_stream.py)
into same directory. Run unit tests to confirm the correctness of your solution.
```sh
cd E_streams # change to directory where stream.py and test_strean.py are
test_url=https://gitlab.bht-berlin.de/sgraupner/ds_cs4bd_2324/-/raw/main/E_streams/test_stream.py
curl -O $(echo $test_url) # download file with unit tests from URL
python test_stream.py # run tests from test file
python -m unittest --verbose # run unit tests with discovery
```
Output:
```sh
Ran 12 tests in 0.001s
OK
Unit testing using test objects:
- test_filter_1()
- test_filter_11()
- test_filter_12()
- test_filter_13()
- test_map_2()
- test_map_21()
- test_reduce_3()
- test_reduce_31()
- test_sort_4()
- test_sort_41()
- test_sort_42()
- test_stream_generation()
---> 12/12 TESTS SUCCEEDED
```
&nbsp;
### 7.) Challenge: Sign-off
For sign-off, change into `E_streams` directory and copy commands into a terminal:
```sh
test_url=https://gitlab.bht-berlin.de/sgraupner/ds_cs4bd_2324/-/raw/main/E_streams/test_stream.py
curl $test_url | python # run tests from URL (use for sign-off)
```
Result:
```
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 6264 100 6264 0 0 38354 0 --:--:-- --:--:-- --:--:-- 38666
Unit testing using test objects:
<stdin>:153: DeprecationWarning: unittest.makeSuite() is deprecated and will be
removed in Python 3.13. Please use unittest.TestLoader.loadTestsFromTestCase() i
nstead.
----------------------------------------------------------------------
Ran 12 tests in 0.001s
OK
```
12 tests succeeded.
(1 Pts)
"""
Special file __init__.py marks a directory as a Python module.
The file is executed once when any .py file is imported.
//
Python unittest require the presence of (even an empty) file.
"""
# load setup module when executed in parent directory
try:
__import__('setup')
#
except ImportError:
pass
import random
"""
Class of a data stream comprised of a sequence of stream operations:
- slice(i1, i2, i3) # slice stream in analogy to python slicing
- filter(filter_func) # pass only elements for which filter_func yields True
- map(map_func) # pass stream where each element is mapped by map_func
- sort(comperator_func) # pass stream sorted by comperator_func
- cond(cond, cond_func) # pass stream or apply conditional function
- print() # pass unchanged stream and print as side effect
and with terminal functions:
- reduce(reduce_func, start) # compound stream to single value with reduce_func
- count() # return number of elements in terminal stream
- get() # return final stream data
"""
class Stream:
def __init__(self, _data=[]):
# constructor to initialize instance member variables
#
self.__streamSource = self.__new_op(_data)
class __Stream_op:
"""
Inner class of one stream operation with chainable functions.
Instances comprise the stream pipeline.
"""
def __init__(self, _new_op_func, _data):
self.__data = _data
self.__new = _new_op_func # __new_op() function injected from outer context
def slice(self, i1, i2=None, i3=1):
# function that returns new __Stream_op instance that slices stream
if i2 == None:
# flip i1, i2 for single arg, e.g. slice(0, 8), slice(8)
i2, i1 = i1, 0
#
# return new __Stream_op instance with sliced __data
return self.__new(self.__data[i1:i2:i3])
def filter(self, filter_func=lambda d : True):
# return new __Stream_op instance that passes only elements for
# which filter_func yields True
#
return self.__new([d for d in self.__data if filter_func(d)])
def map(self, map_func=lambda d : d):
# return new __Stream_op instance that passes elements resulting
# from map_func of corresponding elements in the inbound stream
#
# input data is list of current instance: self.__data
# mapping means a new list needs to be created with same number of
# elements, each obtained by applying map_func
# create new data for next __Stream_op instance from current instance
# data: self.__data
new_data = self.__data # <-- compute new data here
# create new __Stream_op instance with new stream data
new_stream_op_instance = self.__new(new_data)
return new_stream_op_instance
def reduce(self, reduce_func=lambda compound, d : compound + d, start=0) -> any:
# terminal function that returns single value compounded by reduce_func
#
compound = 0 # <-- compute compound result here
return compound
def sort(self, comperator_func=lambda n1, n2 : -1 if n1 < n2 else 1):
# return new __Stream_op instance that passes stream sorted by
# comperator_func
#
# create new data for next __Stream_op instance from current instance
# data: self.__data
new_data = self.__data # <-- compute new data here
# create new __Stream_op instance with new stream data
new_stream_op_instance = self.__new(new_data)
return new_stream_op_instance
def cond(self, cond: bool, conditional):
# return same __Stream_op instance or apply conditional function
# on __Stream_op instance if condition yields True
#
return conditional(self) if cond else self
def print(self, prefix=''):
# return same, unchanged __Stream_op instance and print as side effect
#
print(f'{prefix}{self.__data}')
return self
def count(self) -> int:
# terminal function that returns number of elements in terminal stream
#
return len(self.__data)
def get(self) -> any:
# terminal function that returns final stream __data
#
return self.__data
def source(self):
# return first __Stream_op instance of stream as source
#
return self.__streamSource
def __new_op(self, *argv):
# private method to create new __Stream_op instance
return Stream.__Stream_op(self.__new_op, *argv)
# attempt to load solution module (ignore)
try:
_from, _import = 'stream_sol', 'Stream'
# fetch Stream class from solution, if present
Stream = getattr(__import__(_from, fromlist=[_import]), _import)
#
except ImportError:
pass
if __name__ == '__main__':
run_choice = 3
#
run_choices = {
1: "Challenge 1, Data streams in Python, run the first example",
2: "Challenge 2, complete map() function",
3: "Challenge 3, complete reduce() function",
31: "Challenge 3.1, example RAYCOX",
4: "Challenge 4, complete sort() function",
41: "Challenge 4.1, len-alpha comperator",
42: "Challenge 4.2, tuple output: ('Cox', 'Xoc', 3)",
5: "Challenge 5, Pipeline for product codes",
51: "Challenge 5.1, even digit codes"
}
names = ['Gonzalez', 'Gill', 'Hardin', 'Richardson', 'Buckner', 'Marquez',
'Howe', 'Ray', 'Navarro', 'Talley', 'Bernard', 'Gomez', 'Hamilton',
'Case', 'Petty', 'Lott', 'Casey', 'Hall', 'Pena', 'Witt', 'Joyner',
'Raymond', 'Crane', 'Hendricks', 'Vance', 'Cleveland', 'Duncan', 'Soto',
'Brock', 'Graham', 'Nielsen', 'Rutledge', 'Strong', 'Cox']
if run_choice == 1:
# Challenge 1, Data streams in Python, run the first example
result = Stream(names).source() \
.filter(lambda n : len(n) == 4) \
.print() \
.count()
#
print(f'found {result} names with 4 letters.')
if run_choice == 2:
# Challenge 2, complete map() function
# to map names to name lengths for the first 8 names
Stream(names).source() \
.slice(8) \
.print() \
.map(lambda n : len(n)) \
.print()
if run_choice == 3:
# Challenge 3, complete reduce() function
# to compound all name lengths to a single result
result = Stream(names).source() \
.slice(8) \
.print() \
.map(lambda n : len(n)) \
.print() \
.reduce(lambda x, y : x + y)
#
print(f'compound number of letters in names is: {result}.')
if run_choice == 31:
# Challenge 3.1, example RAYCOX
# compound single string of all n-letter names
n = 3
result = Stream(names).source() \
.filter(lambda name : len(name) == n) \
.print() \
.map(lambda n : n.upper()) \
.reduce(lambda x, y : str(x) + str(y), '')
#
print(f'compounded {n}-letter names: {result}.')
if run_choice == 4:
# Challenge 4, complete sort() function
Stream(names).source() \
.slice(8) \
.print('unsorted: ') \
.sort() \
.print(' sorted: ')
alpha_comperator = lambda n1, n2 : -1 if n1 < n2 else 1
len_alpha_comperator = lambda n1, n2 : -1 if len(n1) < len(n2) else 1 if len(n1) > len(n2) else alpha_comperator(n1, n2)
#
if run_choice == 41:
# Challenge 4.1, len-alpha comperator
Stream(names).source() \
.sort(len_alpha_comperator) \
.print('sorted: ')
if run_choice == 42:
# Challenge 4.2, tuple output: ('Cox', 'Xoc', 3)
result = Stream(names).source() \
.sort(len_alpha_comperator) \
.map(lambda n : (n, n[::-1].capitalize(), len(n))) \
.filter(lambda n1 : n1[2] % 2 == 1) \
.print('sorted: ') \
.count()
#
print(f'\\\\\n{result} odd-length names found.')
# rand_numbers = [random.randint(100000,999999) for i in range(30)]
# print(f'random numbers: {rand_numbers}')
#
if run_choice == 5 or run_choice == 51:
# Challenge 5, Pipeline for product codes
# Challenge 5.1, even digit codes
#
for i in range(1, 5):
# Stream of 5 random numbers from integer range, feel free to change
codes = Stream([random.randint(100000,999999) for j in range(1000)]).source() \
.filter(lambda n : n % 2 == 0) \
.cond( run_choice == 51, \
# use only numbers with even digits, test by split up number in sequence of digits
lambda op : op.filter(lambda n : len(set(map(int, str(n))).intersection([1, 3, 5, 7, 9])) == 0) \
) \
.slice(5) \
.sort() \
.map(lambda n : f'X{n}-{sum(list(map(int, str(n)))) % 10}') \
.get()
#
print(f'batch {i}: {codes}')
import unittest
from stream import Stream
class Stream_test(unittest.TestCase):
"""
Test class.
"""
list_1 = [4, 12, 3, 8, 17, 12, 1, 8, 7]
list_1_str = [str(d) for d in list_1]
names = ['Gonzalez', 'Gill', 'Hardin', 'Richardson', 'Buckner', 'Marquez',
'Howe', 'Ray', 'Navarro', 'Talley', 'Bernard', 'Gomez', 'Hamilton',
'Case', 'Petty', 'Lott', 'Casey', 'Hall', 'Pena', 'Witt', 'Joyner',
'Raymond', 'Crane', 'Hendricks', 'Vance', 'Cleveland', 'Duncan', 'Soto',
'Brock', 'Graham', 'Nielsen', 'Rutledge', 'Strong', 'Cox']
# tests for stream generation function
def test_stream_generation(self):
#
result = Stream(self.list_1).source() \
.get()
self.assertEqual(self.list_1, result)
# tests for filter() function
def test_filter_1(self):
#
# test Challenge 1
result = Stream(self.list_1).source() \
.filter(lambda n : n % 2 == 1) \
.get()
self.assertEqual([3, 17, 1, 7], result)
def test_filter_11(self):
result = Stream(self.list_1).source() \
.filter(lambda d : False) \
.get()
self.assertEqual([], result)
def test_filter_12(self):
result = Stream(self.list_1).source() \
.filter(lambda d : True) \
.get()
self.assertEqual(self.list_1, result)
def test_filter_13(self):
result = Stream(self.names).source() \
.filter(lambda n : len(n) == 4) \
.get()
self.assertEqual(['Gill', 'Howe', 'Case', 'Lott', 'Hall', 'Pena', 'Witt', 'Soto'], result)
# tests for map() function
def test_map_2(self):
#
# test Challenge 2
result = Stream(self.names).source() \
.slice(8) \
.map(lambda n : len(n)) \
.get()
self.assertEqual([8, 4, 6, 10, 7, 7, 4, 3], result)
def test_map_21(self):
result = Stream(self.names).source() \
.filter(lambda n : len(n) == 3) \
.map(lambda n : (n, len(n))) \
.get()
self.assertEqual([('Ray', 3), ('Cox', 3)], result)
# tests for reduce() function
def test_reduce_3(self):
#
# test Challenge 3
result = Stream(self.names).source() \
.slice(8) \
.map(lambda n : len(n)) \
.reduce(lambda x, y : x + y)
self.assertEqual(49, result)
def test_reduce_31(self):
# test Challenge 3.1
n = 3
result = Stream(self.names).source() \
.filter(lambda name : len(name) == n) \
.map(lambda n : n.upper()) \
.reduce(lambda x, y : str(x) + str(y), '')
self.assertEqual('RAYCOX', result)
#
n = 5
result = Stream(self.names).source() \
.filter(lambda name : len(name) == n) \
.map(lambda n : n.upper()) \
.reduce(lambda x, y : str(x) + str(y), '')
self.assertEqual('GOMEZPETTYCASEYCRANEVANCEBROCK', result)
# tests for sort() function
def test_sort_4(self):
# test Challenge 4
result = Stream(self.names).source() \
.slice(8) \
.sort() \
.get()
expected = ['Buckner', 'Gill', 'Gonzalez', 'Hardin', 'Howe', 'Marquez', 'Ray', 'Richardson']
self.assertEqual(expected, result)
def alpha_comperator(self, n1, n2):
return -1 if n1 < n2 else 1
def len_alpha_comperator(self, n1, n2):
return -1 if len(n1) < len(n2) else 1 if len(n1) > len(n2) else self.alpha_comperator(n1, n2)
def test_sort_41(self):
# test Challenge 4.1
result = Stream(self.names).source() \
.sort(self.len_alpha_comperator) \
.get()
#
expected = ['Cox', 'Ray', 'Case', 'Gill', 'Hall', 'Howe', 'Lott', 'Pena', 'Soto', 'Witt',
'Brock', 'Casey', 'Crane', 'Gomez', 'Petty', 'Vance', 'Duncan', 'Graham', 'Hardin',
'Joyner', 'Strong', 'Talley', 'Bernard', 'Buckner', 'Marquez', 'Navarro', 'Nielsen',
'Raymond', 'Gonzalez', 'Hamilton', 'Rutledge', 'Cleveland', 'Hendricks', 'Richardson'
]
self.assertEqual(expected, result)
def test_sort_42(self):
# test Challenge 4.2
result = Stream(self.names).source() \
.sort(self.len_alpha_comperator) \
.map(lambda n : (n, n[::-1].capitalize(), len(n))) \
.filter(lambda n1 : n1[2] % 2 == 1) \
.get()
#
expected = [('Cox', 'Xoc', 3), ('Ray', 'Yar', 3), ('Brock', 'Kcorb', 5), ('Casey', 'Yesac', 5),
('Crane', 'Enarc', 5), ('Gomez', 'Zemog', 5), ('Petty', 'Yttep', 5), ('Vance', 'Ecnav', 5),
('Bernard', 'Dranreb', 7), ('Buckner', 'Renkcub', 7), ('Marquez', 'Zeuqram', 7),
('Navarro', 'Orravan', 7), ('Nielsen', 'Neslein', 7), ('Raymond', 'Dnomyar', 7),
('Cleveland', 'Dnalevelc', 9), ('Hendricks', 'Skcirdneh', 9)
]
self.assertEqual(expected, result)
#
result = Stream(self.names).source() \
.sort(self.len_alpha_comperator) \
.map(lambda n : (n, n[::-1].capitalize(), len(n))) \
.filter(lambda n1 : n1[2] % 2 == 1) \
.count()
self.assertEqual(16, result)
# report results,
# see https://stackoverflow.com/questions/28500267/python-unittest-count-tests
# currentResult = None
# @classmethod
# def setResult(cls, amount, errors, failures, skipped):
# cls.amount, cls.errors, cls.failures, cls.skipped = \
# amount, errors, failures, skipped
# def tearDown(self):
# amount = self.currentResult.testsRun
# errors = self.currentResult.errors
# failures = self.currentResult.failures
# skipped = self.currentResult.skipped
# self.setResult(amount, errors, failures, skipped)
# @classmethod
# def tearDownClass(cls):
# print("\ntests run: " + str(cls.amount))
# print("errors: " + str(len(cls.errors)))
# print("failures: " + str(len(cls.failures)))
# print("success: " + str(cls.amount - len(cls.errors) - len(cls.failures)))
# print("skipped: " + str(len(cls.skipped)))
# def run(self, result=None):
# self.currentResult = result # remember result for use in tearDown
# unittest.TestCase.run(self, result) # call superclass run method
if __name__ == '__main__':
result = unittest.main()
# Assignment F: Graph Data &nbsp; (10 Pts)
### Challenges
- [Challenge 1:](#1-challenge-understanding-graph-data) Understanding Graph Data
- [Challenge 2:](#2-challenge-representing-graph-data-in-python) Representing Graph Data in Python
- [Challenge 3:](#3-challenge-implementing-the-graph-in-python) Implementing the Graph in Python
- [Challenge 4:](#4-challenge-implementing-dijkstras-shortest-path-algorithm) Implementing Dijkstra's Shortest Path Algorithm
- [Challenge 5:](#5-challenge-run-for-another-graph) Run for Another Graph
Points: [1, 1, 2, 4, 2]
&nbsp;
### 1.) Challenge: Understanding Graph Data
A *[Graph](https://en.wikipedia.org/wiki/Graph_theory)*
is a set of nodes (vertices) and edges connecting nodes G = { n ∈ N, e ∈ E }.
A *weighted Graph* has a *weight* (number) associated to each egde.
A *[Path](https://en.wikipedia.org/wiki/Path_(graph_theory))*
is a subset of edges that connects a subset of nodes.
We consider Complete Graphs where all nodes can be reached from any other
node by at least one path (no disconnected subgraphs).
Graphs may have cycles (paths that lead to nodes visited before) or
paths may join at nodes that are part of other paths.
Traversal is the process of visiting each node of a graph exactly once.
Multiple visits of graph nodes by cycles or joins must be detected by
marking visited nodes (which is not preferred since it alters the data set)
or by keeping a separate record of visits.
Write two properties that distinguish graphs from trees.
(1 Pt)
&nbsp;
### 2.) Challenge: Representing Graph Data in Python
Python has no built-in data type that supports graph data.
Separate packages my be used such as
[NetworkX](https://networkx.org/).
In this assignment, we focus on basic Python data structures.
1. How can Graphs be represented in general?
1. How can these by implemented using Python base data structures?
1. Which data structure would be efficient giving the fact that in the
example below that graph is constant and only traversal operations
are performed?
(1 Pt)
&nbsp;
### 3.) Challenge: Implementing the Graph in Python
Watch the video and understand how
[Dijkstra's Shortest Path Algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)
(1956) works and which information it needs.
*Edsger W. Dijkstra* (1930-2003,
[bio](https://en.wikipedia.org/wiki/Edsger_W._Dijkstra))
was a Dutch computer scientist, programmer and software engineer.
He was a professor of Computer Science at the Univerity of Austin, Texas
and has received numerous awards, including the
[Turing Award](https://en.wikipedia.org/wiki/Turing_Award)
in 1972.
<!--
[video (FelixTechTips)](https://youtu.be/bZkzH5x0SKU?si=n8Z2ZIfbB73_v1TE)
<img src="../markup/img/graph_2a.jpg" alt="drawing" width="640"/>
-->
[Video (Mike Pound, Computerphile)](https://youtu.be/GazC3A4OQTE?si=ZuBEcWaBzuKmPMqA)
<img src="../markup/img/graph_1.jpg" alt="drawing" width="640"/>
Node `S` forms the start of the algorithm, node `E` is the destination.
Draw a sketch of the data structures needed to represent the graph with
nodes, edges and weights and also the data needed for the algorithm.
Create a Python file `shortest_path.py` with
- declarations of data structures you may need for the graph and
information for the algorithm and
- data to represent the graph in the video with nodes: {A ... K, S} and
the shown edges with weights.
(2 Pts)
&nbsp;
### 4.) Challenge: Implementing Dijkstra's Shortest Path Algorithm
Implement Dijkstra's Algorithm.
Output the sortest path as sequence of nodes, followed by an analysis and
the shortest distance.
```
shortest path: S -> B -> H -> G -> E
analysis:
S->B(2)
B->H(1)
H->G(2)
G->E(2)
shortest distance is: 7
```
(4 Pts)
&nbsp;
### 5.) Challenge: Run for Another Graph
Run your algorithm for another graph G: {A ... F} with weights:
```
G: {A, B, C, D, E, F}, start: A, end: C
Weights:
AB(2), BE(6), EC(9), AD(8), BD(5),
DE(3), DF(2), EF(1), FC(3)
```
Output the result:
```
shortest path: A -> B -> D -> F -> C
analysis:
S->B(2)
B->D(5)
D->F(2)
F->C(3)
shortest distance is: 12
```
(2 Pts)
# Assignment G: Docker &nbsp; (18 Pts)
This assignment will setup Docker. If you already have it, simply run challenges and answer questions.
Docker is a popular software packaging, distribution and execution infrastructure using containers.
- Docker runs on Linux only (LXC). Mac, Windows have built adapter technologies.
- Windows uses an internal Linux VM to run Docker engine ( *dockerd* ).
- Client tools (CLI, GUI, e.g.
[Docker Desktop](https://docs.docker.com/desktop/install/windows-install/)
for Windows) are used to manage and execute containers.
Docker builds on Linux technologies:
- stackable layers of filesystem images that each contain only a diff to an underlying image.
- tools to build, manage and distribute layered images ("ship containers").
- Linux LXC technology to “execute containers” as groups of isolated processes on a Linux system (create/run a new container, start/stop/join container).
Salomon Hykes, PyCon 2013, Santa Clara CA: *"The Future of Linux Containers"* ([watch](https://www.youtube.com/watch?v=9xciauwbsuo), 5:21min).
### Challenges
- [Challenge 1:](#1-challenge-docker-setup-and-cli) Docker Setup and CLI
- [Challenge 2:](#2-challenge-run-hello-world-container) Run *hello-world* Container
- [Challenge 3:](#3-challenge-run-minimal-alpine-python-container) Run minimal (Alpine) Python Container
- [Challenge 4:](#4-challenge-configure-alpine-container-for-ssh) Configure Alpine Container for *ssh*
- [Challenge 5:](#5-challenge-build-alpine-python-container-with-ssh-access) Build Alpine-Python Container with *ssh*-Access
- [Challenge 6:](#6-challenge-setup-ide-to-develop-code-in-alpine-python-container) Setup IDE to develop Code in Alpine-Python Container
- [Challenge 7:](#7-challenge-run-jupyter-in-docker-container) Run *Jupyter* in Docker Container
Points: [2, 2, 2, 4, 4, 2, 2]
&nbsp;
### 1.) Challenge: Docker Setup and CLI
[Docker Desktop](https://docs.docker.com/desktop)
bundles all necessary Docker components necessary to run Docker on your
system (Windows, Mac, Linux). It comes with a GUI that makes using Docker
easier and is recommended for beginners.
Components can also be installed individually (e.g. "Docker Engine"), but this
may involve installation of dependencies such as the WSL virtual machine on Windows.
Docker CLI
Docker CLI is the Docker command-line interface that is needed to run docker
commands in a terminal.
After setting up Docker Desktop, open a terminal and type commands:
```sh
> docker --version
Docker version 20.10.17, build 100c701
> docker --help
...
> docker ps ; dockerd is not running
error during connect: This error may indicate that the docker daemon is not runn
ing.
> docker ps ; dockerd is now running, no containers yet
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
If you can't run the `docker` command, the client-side **docker-CLI** (Command-Line-Interface) may not be installed or not on the PATH-variable. If `docker ps` says: "can't connect", the **Docker engine** (server-side: *dockerd* ) is not running and must be started.
(2 Pts)
&nbsp;
### 2.) Challenge: Run *hello-world* Container
Run the *hello-world* container from Docker-Hub: [hello-world](https://hub.docker.com/_/hello-world):
```sh
> docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:62af9efd515a25f84961b70f973a798d2eca956b1b2b026d0a4a63a3b0b6a3f2
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
```
Show the container image loaded on your system:
```sh
> docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest feb5d9fea6a5 12 months ago 13.3kB
```
Show that the container is still present after the end of execution:
```sh
> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
da16000022e0 hello-world "/hello" 6 min ago Exited(0) magical_aryabhata
```
Re-start the container with an attached (-a) *stdout* terminal.
Refer to the container either by its ID ( *da16000022e0* ) or by its
generated NAME ( *magical_aryabhata* ).
```sh
> docker start da16000022e0 -a or: docker start magical_aryabhata -a
Hello from Docker!
This message shows that your installation appears to be working correctly.
```
Re-run will create a new container and execut it. `docker ps -a ` will then
show two containers created from the same image.
```sh
> docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
da16000022e0 hello-world "/hello" 6 min ago Exited(0) magical_aryabhata
40e605d9b027 hello-world "/hello" 4 sec ago Exited(0) pedantic_rubin
```
"Run" always creates new containers while "start" restarts existing containers.
(2 Pts)
&nbsp;
### 3.) Challenge: Run minimal (Alpine) Python Container
[Alpine](https://www.alpinelinux.org) is a minimal base image that has become
popular for building lean containers (few MB as opposed to 100's of MB or GB's).
Being mindful of resources is important for container deployments in cloud
environments where large numbers of containers are deployed and resource use
is billed.
Pull the latest Alpine image from Docker-Hub (no container is created with just
pulling the image). Mind image sizes: hello-world (13.3kB), alpine (5.54MB).
```sh
> docker pull alpine:latest
docker pull alpine:latest
latest: Pulling from library/alpine
Digest: sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad
Status: Image is up to date for alpine:latest
docker.io/library/alpine:latest
> docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world latest feb5d9fea6a5 12 months ago 13.3kB
alpine latest 9c6f07244728 8 weeks ago 5.54MB
```
Create and run an Alpine container executing an interactive shell `/bin/sh` attached to the terminal ( `-it` ). It launches the shell that runs commands inside the Alpine
container.
```sh
> docker run -it alpine:latest /bin/sh
# ls -la
total 64
drwxr-xr-x 1 root root 4096 Oct 5 18:32 .
drwxr-xr-x 1 root root 4096 Oct 5 18:32 ..
-rwxr-xr-x 1 root root 0 Oct 5 18:32 .dockerenv
drwxr-xr-x 2 root root 4096 Aug 9 08:47 bin
drwxr-xr-x 5 root root 360 Oct 5 18:32 dev
drwxr-xr-x 1 root root 4096 Oct 5 18:32 etc
drwxr-xr-x 2 root root 4096 Aug 9 08:47 home
drwxr-xr-x 7 root root 4096 Aug 9 08:47 lib
drwxr-xr-x 5 root root 4096 Aug 9 08:47 media
drwxr-xr-x 2 root root 4096 Aug 9 08:47 mnt
drwxr-xr-x 2 root root 4096 Aug 9 08:47 opt
dr-xr-xr-x 179 root root 0 Oct 5 18:32 proc
drwx------ 1 root root 4096 Oct 5 18:36 root
drwxr-xr-x 2 root root 4096 Aug 9 08:47 run
drwxr-xr-x 2 root root 4096 Aug 9 08:47 sbin
drwxr-xr-x 2 root root 4096 Aug 9 08:47 srv
dr-xr-xr-x 13 root root 0 Oct 5 18:32 sys
drwxrwxrwt 2 root root 4096 Aug 9 08:47 tmp
drwxr-xr-x 7 root root 4096 Aug 9 08:47 usr
drwxr-xr-x 12 root root 4096 Aug 9 08:47 var
# whoami
root
# uname -a
Linux aab69035680f 5.10.124-linuxkit #1 SMP Thu Jun 30 08:19:10 UTC 2022 x86_64
# exit
```
Commands after the `#` prompt (*root* prompt) are executed by the `/bin/sh` shell
inside the container.
`# exit` ends the shell process and returns to the surrounding shell. The container
will go into a dormant (inactive) state.
```sh
> docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aab69035680f alpine:latest "/bin/sh" 9 min ago Exited boring_ramanujan
```
The container can be restarted with any number of `/bin/sh` shell processes.
Containers are executed by **process groups** - so-called
[cgroups](https://en.wikipedia.org/wiki/Cgroups) used by
[LXC](https://wiki.gentoo.org/wiki/LXC) -
that share the same environment (filesystem view, ports, etc.), but are isolated
from process groups of other containers.
Start a shell process in the dormant Alpine-container to re-activate.
The start command will execute the default command that is built into the container
(see the COMMAND column: `"/bin/sh"`). The option `-ai` attaches *stdout* and *stdin*
of the terminal to the container.
Write *"Hello, container"* into a file: `/tmp/hello.txt`. Don't leave the shell.
```sh
> docker start aab69035680f -ai
# echo "Hello, container!" > /tmp/hello.txt
# cat /tmp/hello.txt
Hello, container!
#
```
Start another shell in another terminal for the container. Since it refers to the same
container, both shell processes share the same filesystem.
The second shell can therefore see the file created by the first and append another
line, which again will be seen by the first shell.
```sh
> docker start aab69035680f -ai
# cat /tmp/hello.txt
Hello, container!
# echo "How are you?" >> /tmp/hello.txt
```
First terminal:
```sh
# cat /tmp/hello.txt
Hello, container!
How are you?
#
```
In order to perform other commands than the default command in a running container,
use `docker exec`.
Execute command: `cat /tmp/hello.txt` in a third terminal:
```sh
docker exec aab69035680f cat /tmp/hello.txt
Hello, container!
How are you?
```
The execuition creates a new process that runs in the container seeing its filesystem
and other resources.
Explain the next command:
- What is the result?
- How many processes are involved?
- Draw a skech with the container, processes and their stdin/-out connections.
```sh
echo "echo That\'s great to hear! >> /tmp/hello.txt" | \
docker exec -i aab69035680f /bin/sh
```
When all processes have exited, the container will return to the dormant state.
It will preserve the created file.
(2 Pts)
&nbsp;
### 4.) Challenge: Configure Alpine Container for *ssh*
Create a new Alpine container with name `alpine-ssh` and configure it for
[ssh](https://en.wikipedia.org/wiki/Secure_Shell) access.
```sh
docker run --name alpine-ssh -p 22:22 -it alpine:latest
```
Instructions for installation and confiduration can be found here:
["How to install OpenSSH server on Alpine Linux"](https://www.cyberciti.biz/faq/how-to-install-openssh-server-on-alpine-linux-including-docker) or here:
["Setting up a SSH server"](https://wiki.alpinelinux.org/wiki/Setting_up_a_SSH_server).
Add a local user *larry* with *sudo*-rights, install *sshd* listening on the
default port 22.
Write down commands that you used for setup and configuration to enable the
container to run *sshd*.
Verify that *sshd* is running in the container:
```sh
# ps -a
PID USER TIME COMMAND
1 root 0:00 /bin/sh
254 root 0:00 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups
261 root 0:00 ps -a
```
Show that *ssh* is working by login in as *larry* from another terminal:
```sh
> ssh larry@localhost
Welcome to Alpine!
The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org/>.
You can setup the system with the command: setup-alpine
You may change this message by editing /etc/motd.
54486c62d745:~$ whoami
larry
54486c62d745:~$ ls -la
total 32
drwxr-sr-x 1 larry larry 4096 Oct 2 21:34 .
drwxr-xr-x 1 root root 4096 Oct 2 20:40 ..
-rw------- 1 larry larry 602 Oct 5 18:53 .ash_history
54486c62d745:~$ uname -a
Linux 54486c62d745 5.10.124-linuxkit #1 SMP Thu Jun 30 08:19:10 UTC 2022 x86_64 Linux
54486c62d745:~$
```
(4 Pts)
&nbsp;
### 5.) Challenge: Build Alpine-Python Container with *ssh*-Access
[`python:latest`](https://hub.docker.com/_/python/tags) official image is 340MB while [`python:3.9.0-alpine`](https://hub.docker.com/_/python/tags?name=3.9-alpine&page=1) is ~18MB. The alpine-version builds on minimal Alpine Linux while the official version builds on Ubuntu. "Minimal" means available commands, tools inside the container is restricted. Only basic tools are available. Required additional tools need to be installed into the container.
Build an new ```alpine-python-sshd``` container based on the ```python:3.9.0-alpine``` image that includes Python 3.9.0 and ssh-access so that your IDE can remotely connect to the container and run/debug Python code inside the container, which is the final challenge.
Copy file [print_sys.py](https://github.com/sgra64/cs4bigdata/blob/main/A_setup_python/print_sys.py) from Assignment A into larry's ```$HOME``` directory and execute.
```sh
> ssh larry@localhost
Welcome to Alpine!
54486c62d745:~$ python print_sys.py
Python impl: CPython
Python version: #1 SMP Thu Jun 30 08:19:10 UTC 2022
Python machine: x86_64
Python system: Linux
Python version: 3.9.0
54486c62d745:~$
```
(4 Pts)
&nbsp;
### 6.) Challenge: Setup IDE to develop Code in Alpine-Python Container
Setup your IDE to run/debug Python code inside the `alpine-python-sshd` container. In Visual Studio Code (with extensions for "Remote Development", "Docker" and "Dev Containers"), go to the Docker side-Tab, Right-click the running container and "Attach Visual Studio Code". This opens a new VSCode Window with a view from inside the container with file [print_sys.py](https://github.com/sgra64/cs4bigdata/blob/main/A_setup_python/print_sys.py) from the previous challenge.
Run this file in the IDE connected to the container. Output will show it running under Linux, Python 3.9.0 in `/home/larry`.
<!-- ![Remote Code](Setup_img01.png) -->
<img src="../markup/img/G_docker_img01.png" alt="drawing" width="640"/>
(2 Pts)
&nbsp;
### 7.) Challenge: Run *Jupyter* in Docker Container
Setup a Jupyter-server from the [Jupyter Docker Stack](https://jupyter-docker-stacks.readthedocs.io/en/latest/index.html). Jupyter Docker Stacks are a set of ready-to-run Docker images containing Jupyter applications and interactive computing tools.
[Selecting an Image](https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html) decides about the features preinstalled for Jupyter. Configurations exit for *all-spark-notebook* building on *pyspark-notebook* building on *scipy-notebook*, which builds on a *minimal-* and *base-notebook*, which builds on an *Ubuntu LTS* distribution. Other variations exist for *tensorflow-*, *datascience-*, or *R-notebooks*.
![Remote Code](https://jupyter-docker-stacks.readthedocs.io/en/latest/_images/inherit.svg)
Pull the image for the *minimal-notebook* (415 MB, [tags](https://hub.docker.com/r/jupyter/minimal-notebook/tags/) ) and start it.
```sh
docker pull jupyter/minimal-notebook:latest
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
jupyter/minimal-notebook latest 33f2fa3eb079 18h ago 1.39GB
```
Create the container with Jupyters default port 8888 exposed to the host environment.
Watch for the URL with the token in the output log.
```sh
docker run --name jupyter-minimal -p 8888:8888 jupyter/minimal-notebook
Entered start.sh with args: jupyter lab
Executing the command: jupyter lab
[I 2022-10-10 21:53:22.855 ServerApp] jupyterlab | extension was successfully linked.
...
To access the server, open this file in a browser:
http://127.0.0.1:8888/lab?token=6037ff448a79463b97e3c29af712b9395dd8548b
71d77769
```
After the first access with the token included in the URL, the browser
opens with http://localhost:8888/lab.
<!-- ![Remote Code](Setup_img02.png) -->
<img src="../markup/img/G_docker_img02.png" alt="drawing" width="640"/>
Start to work with Jupyter. A Jupyter notebook is a web-form comprised
of cells where Python commands can be entered. Execution is triggered
by `SHIFT + Enter`. Run the Code below (copy & paste from file *print_sys.py* ).
<!-- ![Remote Code](Setup_img03.png) -->
<img src="../markup/img/G_docker_img03.png" alt="drawing" width="640"/>
The notebook is stored **inside** the container under *Untitled.ipynb*.
Shut down the Jupyter-server with: File -> Shutdown.
Reatart the (same) container as "daemon" process running in the background (the container remembers flags given at creation: `-p 8888:8888`). The flag `-a` attaches a terminal to show log lines with the token-URL.
```sh
docker start jupyter-minimal -a
```
After login, the prior notebook with the code above is still there under *Untitled.ipynb*. Open and re-run the notebook.
A container preserves its state, e.g. files that get created. Docker
simply adds them in another image layer. Therefore, the size of a
container is only defined by state changes that occured after the
creation of the container instance.
Shut down the Jupyter server with: File -> Shutdown, not with:
```sh
docker stop jupyter-minimal
```
(2 Pts)
#######################################################################
# Dockerfile to build image for Python-Alpine container that also has
# ssh access.
#
# Use python:alpine image and install needed packages for ssh:
# - openrc, system services control system
# - openssh, client- and server side ssh
# - sudo, utility to enable root rights to users
#
FROM python:3.9.0-alpine
RUN apk update
RUN apk add --no-cache openrc
RUN apk add --update --no-cache openssh
RUN apk add --no-cache sudo
# adjust sshd configuration
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config
RUN echo 'IgnoreUserKnownHosts yes' >> /etc/ssh/sshd_config
# add user larry with empty password
RUN adduser -h /home/larry -s /bin/sh -D larry
RUN echo -n 'larry:' | chpasswd
# add larry to sudo'ers list
RUN mkdir -p /etc/sudoers.d
RUN echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/wheel
RUN adduser larry wheel
# generate host key
RUN ssh-keygen -A
# add sshd as service, start on boot [default], touch file to prevent error:
# "You are attempting to run an openrc service on a system which openrc did not boot."
RUN rc-update add sshd default
RUN mkdir -p /run/openrc
RUN touch /run/openrc/softlevel
# sshd is started in /entrypoint.sh
#
#######################################################################
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 22
COPY entrypoint.sh /
#!/bin/sh
# ssh-keygen -A
exec /usr/sbin/sshd -D -e "$@"
#######################################################################
# Dockerfile to build image for Alpine container that has sshd daemon.
#
# Use bare Alpine image and install all needed packages:
# - openrc, system services control system
# - openssh, client- and server side ssh
# - sudo, utility to enable root rights to users
#
FROM alpine:latest
RUN apk update
RUN apk add --no-cache openrc
RUN apk add --update --no-cache openssh
RUN apk add --no-cache sudo
# adjust sshd configuration
RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd_config
RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config
RUN echo 'IgnoreUserKnownHosts yes' >> /etc/ssh/sshd_config
# add user larry with empty password
RUN adduser -h /home/larry -s /bin/sh -D larry
RUN echo -n 'larry:' | chpasswd
# add larry to sudo'ers list
RUN mkdir -p /etc/sudoers.d
RUN echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers.d/wheel
RUN adduser larry wheel
# generate host key
RUN ssh-keygen -A
# add sshd as service, start on boot [default], touch file to prevent error:
# "You are attempting to run an openrc service on a system which openrc did not boot."
RUN rc-update add sshd default
RUN mkdir -p /run/openrc
RUN touch /run/openrc/softlevel
# sshd is started in /entrypoint.sh
#
#######################################################################
ENTRYPOINT ["/entrypoint.sh"]
EXPOSE 22
COPY entrypoint.sh /
### Docker-compose
[Docker-compose](https://docs.docker.com/compose/features-uses) is a tool set
for Docker to automate building, configuring and running containers from a single file:
[docker-compose.yaml](https://docs.docker.com/compose/compose-file) specification.
Containers are referred to as *services* in the Docker-compose specification.
When a specified image does not exist and the build-tag of a service refers to a
directory where the container can be built from a
[Dockerfile](https://docs.docker.com/engine/reference/builder) (must be in same
directory of `docker-compose.yaml`):
```
docker-compose up -d
```
will automatically perform these steps (`-d` starts container in background):
1. build the image from `Dockerfile`,
1. register the image locally,
1. create a new container from the image,
1. register the container locally and
1. start it.
Multilpe containers can be specified in a single `docker-compose.yaml` file and
started in a defined order expressed by dependencies (`depends_on`-tag, e.g. to
express that a database service must be started before an application service
that is depending on it).
To stop all services specified in a `docker-compose.yaml` file:
```
docker-compose stop
```
To (re-)start services and show their running states:
```
docker-compose start
docker-compose ps
```
The `alpine-sshd` container can therefore always fully be re-produced from the
specifications in this directory.
Images and containers should always be reproduceable. They can be deleted any
time and recovered from specifications.
Container specifications are therefore common in code repositories controlling
automated *build-* and *deployment*-processes.
The principle implies that state ("data" such as databases) should not be
stored in containers and rather reside on outside volumes that are
[mounted](https://docs.docker.com/storage/volumes)
into the container.
Build and start `alpine-sshd` container from scratch:
```
docker-compose up
[+] Running 0/1
- alpine-sshd Error 2.5s
[+] Building 0.3s (23/23) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 32B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/alpine:latest 0.0s
=> [ 1/18] FROM docker.io/library/alpine:latest 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 34B 0.0s
=> CACHED [ 2/18] RUN apk update 0.0s
=> CACHED [ 3/18] RUN apk add --no-cache openrc 0.0s
=> CACHED [ 4/18] RUN apk add --update --no-cache openssh 0.0s
=> CACHED [ 5/18] RUN apk add --no-cache sudo 0.0s
=> CACHED [ 6/18] RUN echo 'PasswordAuthentication yes' >> /etc/ssh/sshd 0.0s
=> CACHED [ 7/18] RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_c 0.0s
=> CACHED [ 8/18] RUN echo 'IgnoreUserKnownHosts yes' >> /etc/ssh/sshd_c 0.0s
=> CACHED [ 9/18] RUN adduser -h /home/larry -s /bin/sh -D larry 0.0s
=> CACHED [10/18] RUN echo -n 'larry:' | chpasswd 0.0s
=> CACHED [11/18] RUN mkdir -p /etc/sudoers.d 0.0s
=> CACHED [12/18] RUN echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudo 0.0s
=> CACHED [13/18] RUN adduser larry wheel 0.0s
=> CACHED [14/18] RUN ssh-keygen -A 0.0s
=> CACHED [15/18] RUN rc-update add sshd default 0.0s
=> CACHED [16/18] RUN mkdir -p /run/openrc 0.0s
=> CACHED [17/18] RUN touch /run/openrc/softlevel 0.0s
=> CACHED [18/18] COPY entrypoint.sh / 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => writing image sha256:5664d856423d679de32c4b58fc1bb55d5973acb62507d 0.0s
=> => naming to docker.io/library/alpine-sshd 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and l
earn how to fix them
[+] Running 2/2
- Network alpine-sshd_default C... 0.1s
- Container alpine-sshd-alpine-sshd-1 Created 0.2s
Attaching to alpine-sshd-alpine-sshd-1
alpine-sshd-alpine-sshd-1 | Server listening on 0.0.0.0 port 22.
alpine-sshd-alpine-sshd-1 | Server listening on :: port 22.
```
Show running container:
```
docker-compose ps
NAME COMMAND SERVICE STATUS PORTS
alpine-sshd-alpine-sshd-1 "/entrypoint.sh" alpine-sshd running 0.0.0.0:22->22/tcp
```
Log in as user *larry* that was configured when the container was built from
the `Dockerfile`:
```
ssh larry@localhost
```
Output:
```
ssh larry@localhost
The authenticity of host 'localhost (::1)' can't be established.
ED25519 key fingerprint is SHA256:5ZZ4bnRJh3DxlDaWJooC1qYjKj00U+pHCuNGEWZPVqA.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Could not create directory '/cygdrive/c/Sven1/svgr/.ssh' (No such file or directory).
Failed to add the host to the list of known hosts (/cygdrive/c/Sven1/svgr/.ssh/known_hosts).
Welcome to Alpine!
The Alpine Wiki contains a large amount of how-to guides and general
information about administrating Alpine systems.
See <http://wiki.alpinelinux.org/>.
You can setup the system with the command: setup-alpine
You may change this message by editing /etc/motd.
85dbbb7c316a:~$ ls -la
total 16
drwxr-sr-x 1 larry larry 4096 Nov 1 12:48 .
drwxr-xr-x 1 root root 4096 Oct 7 18:31 ..
-rw------- 1 larry larry 7 Nov 1 12:48 .ash_history
85dbbb7c316a:~$ whoami
larry
85dbbb7c316a:~$ pwd
/home/larry
85dbbb7c316a:~$
```
Stop container:
```
docker-compose stop
- Container alpine-sshd-alpine-sshd-1 Stopped 0.3s
docker-compose ps
NAME COMMAND SERVICE STATUS
PORTS
alpine-sshd-alpine-sshd-1 "/entrypoint.sh" alpine-sshd exited (0)
```
Restart same container:
```
docker-compose start
[+] Running 1/1
- Container alpine-sshd-alpine-sshd-1 Started 0.4s
docker-compose ps
NAME COMMAND SERVICE STATUS PORTS
alpine-sshd-alpine-sshd-1 "/entrypoint.sh" alpine-sshd running 0.0.0.0:22->22/tcp
```