diff --git a/python/README.md b/python/README.md
index 691f7eb4..01486a22 100644
--- a/python/README.md
+++ b/python/README.md
@@ -38,3 +38,4 @@
- [saddle-points](./saddle-points/README.md)
- [leap](./leap/README.md)
- [mecha-munch-management](./mecha-munch-management/README.md)
+- [plane-tickets](./plane-tickets/README.md)
diff --git a/python/plane-tickets/.coverage b/python/plane-tickets/.coverage
new file mode 100644
index 00000000..824ef023
Binary files /dev/null and b/python/plane-tickets/.coverage differ
diff --git a/python/plane-tickets/.coverage.xml b/python/plane-tickets/.coverage.xml
new file mode 100644
index 00000000..b4c28f03
--- /dev/null
+++ b/python/plane-tickets/.coverage.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/python/plane-tickets/.coveragerc b/python/plane-tickets/.coveragerc
new file mode 100644
index 00000000..883531db
--- /dev/null
+++ b/python/plane-tickets/.coveragerc
@@ -0,0 +1,2 @@
+[run]
+omit = __init__.py, *_test.py
diff --git a/python/plane-tickets/.exercism/config.json b/python/plane-tickets/.exercism/config.json
new file mode 100644
index 00000000..12228348
--- /dev/null
+++ b/python/plane-tickets/.exercism/config.json
@@ -0,0 +1,23 @@
+{
+ "authors": [
+ "J08K"
+ ],
+ "contributors": [
+ "BethanyG",
+ "kytrinyx",
+ "meatball133"
+ ],
+ "files": {
+ "solution": [
+ "generators.py"
+ ],
+ "test": [
+ "generators_test.py"
+ ],
+ "exemplar": [
+ ".meta/exemplar.py"
+ ]
+ },
+ "icon": "new-passport",
+ "blurb": "Learn about generators by assigning seats to passengers on Anaconda Airlines."
+}
diff --git a/python/plane-tickets/.exercism/metadata.json b/python/plane-tickets/.exercism/metadata.json
new file mode 100644
index 00000000..9745fcd4
--- /dev/null
+++ b/python/plane-tickets/.exercism/metadata.json
@@ -0,0 +1 @@
+{"track":"python","exercise":"plane-tickets","id":"64bebba2c2354807b821de9b93c9cfab","url":"https://exercism.org/tracks/python/exercises/plane-tickets","handle":"vpayno","is_requester":true,"auto_approve":false}
\ No newline at end of file
diff --git a/python/plane-tickets/.pylintrc b/python/plane-tickets/.pylintrc
new file mode 120000
index 00000000..30b33b52
--- /dev/null
+++ b/python/plane-tickets/.pylintrc
@@ -0,0 +1 @@
+../.pylintrc
\ No newline at end of file
diff --git a/python/plane-tickets/HELP.md b/python/plane-tickets/HELP.md
new file mode 100644
index 00000000..3ed49f32
--- /dev/null
+++ b/python/plane-tickets/HELP.md
@@ -0,0 +1,130 @@
+# Help
+
+## Running the tests
+
+We use [pytest][pytest: Getting Started Guide] as our website test runner.
+You will need to install `pytest` on your development machine if you want to run tests for the Python track locally.
+You should also install the following `pytest` plugins:
+
+- [pytest-cache][pytest-cache]
+- [pytest-subtests][pytest-subtests]
+
+Extended information can be found in our website [Python testing guide][Python track tests page].
+
+
+### Running Tests
+
+To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_).
+Test files usually end in `_test.py`, and are the same tests that run on the website when a solution is uploaded.
+
+Linux/MacOS
+```bash
+$ cd {path/to/exercise-folder-location}
+```
+
+Windows
+```powershell
+PS C:\Users\foobar> cd {path\to\exercise-folder-location}
+```
+
+
+
+Next, run the `pytest` command in your terminal, replacing `{exercise_test.py}` with the name of the test file:
+
+Linux/MacOS
+```bash
+$ python3 -m pytest -o markers=task {exercise_test.py}
+==================== 7 passed in 0.08s ====================
+```
+
+Windows
+```powershell
+PS C:\Users\foobar> py -m pytest -o markers=task {exercise_test.py}
+==================== 7 passed in 0.08s ====================
+```
+
+
+### Common options
+- `-o` : override default `pytest.ini` (_you can use this to avoid marker warnings_)
+- `-v` : enable verbose output.
+- `-x` : stop running tests on first failure.
+- `--ff` : run failures from previous test before running other test cases.
+
+For additional options, use `python3 -m pytest -h` or `py -m pytest -h`.
+
+
+### Fixing warnings
+
+If you do not use `pytest -o markers=task` when invoking `pytest`, you might receive a `PytestUnknownMarkWarning` for tests that use our new syntax:
+
+```bash
+PytestUnknownMarkWarning: Unknown pytest.mark.task - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
+```
+
+To avoid typing `pytest -o markers=task` for every test you run, you can use a `pytest.ini` configuration file.
+We have made one that can be downloaded from the top level of the Python track directory: [pytest.ini][pytest.ini].
+
+You can also create your own `pytest.ini` file with the following content:
+
+```ini
+[pytest]
+markers =
+ task: A concept exercise task.
+```
+
+Placing the `pytest.ini` file in the _root_ or _working_ directory for your Python track exercises will register the marks and stop the warnings.
+More information on pytest marks can be found in the `pytest` documentation on [marking test functions][pytest: marking test functions with attributes] and the `pytest` documentation on [working with custom markers][pytest: working with custom markers].
+
+Information on customizing pytest configurations can be found in the `pytest` documentation on [configuration file formats][pytest: configuration file formats].
+
+
+### Extending your IDE or Code Editor
+
+Many IDEs and code editors have built-in support for using `pytest` and other code quality tools.
+Some community-sourced options can be found on our [Python track tools page][Python track tools page].
+
+[Pytest: Getting Started Guide]: https://docs.pytest.org/en/latest/getting-started.html
+[Python track tools page]: https://exercism.org/docs/tracks/python/tools
+[Python track tests page]: https://exercism.org/docs/tracks/python/tests
+[pytest-cache]:http://pythonhosted.org/pytest-cache/
+[pytest-subtests]:https://github.com/pytest-dev/pytest-subtests
+[pytest.ini]: https://github.com/exercism/python/blob/main/pytest.ini
+[pytest: configuration file formats]: https://docs.pytest.org/en/6.2.x/customize.html#configuration-file-formats
+[pytest: marking test functions with attributes]: https://docs.pytest.org/en/6.2.x/mark.html#raising-errors-on-unknown-marks
+[pytest: working with custom markers]: https://docs.pytest.org/en/6.2.x/example/markers.html#working-with-custom-markers
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit generators.py` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [Python track's documentation](https://exercism.org/docs/tracks/python)
+- The [Python track's programming category on the forum](https://forum.exercism.org/c/programming/python)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+Below are some resources for getting help if you run into trouble:
+
+- [The PSF](https://www.python.org) hosts Python downloads, documentation, and community resources.
+- [The Exercism Community on Discord](https://exercism.org/r/discord)
+- [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community.
+- [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners.
+- [#python on Libera.chat](https://www.python.org/community/irc/) this is where the core developers for the language hang out and get work done.
+- [Python Community Forums](https://discuss.python.org/)
+- [Free Code Camp Community Forums](https://forum.freecodecamp.org/)
+- [CodeNewbie Community Help Tag](https://community.codenewbie.org/t/help)
+- [Pythontutor](http://pythontutor.com/) for stepping through small code snippets visually.
+
+Additionally, [StackOverflow](http://stackoverflow.com/questions/tagged/python) is a good spot to search for your problem/question to see if it has been answered already.
+ If not - you can always [ask](https://stackoverflow.com/help/how-to-ask) or [answer](https://stackoverflow.com/help/how-to-answer) someone else's question.
\ No newline at end of file
diff --git a/python/plane-tickets/HINTS.md b/python/plane-tickets/HINTS.md
new file mode 100644
index 00000000..e00e7f76
--- /dev/null
+++ b/python/plane-tickets/HINTS.md
@@ -0,0 +1,24 @@
+# Hints
+
+## 1. Generate seat letters
+
+- The returned value should be of _type_ `generator`.
+- You can have a sequence of letters from `A` to `D` and cycle through them.
+- Use `yield` to return the next letter.
+
+## 2. Generate seats
+
+- The returned value should be of _type_ `generator`.
+- Row `13` should be skipped, so go from `12` to `14`.
+- Keep in mind that the returned values should be ordered from low to high. `1A, 1B, 2A, ...`
+- Here it might be good to reuse or call functions you have already defined.
+
+## 3. Assign seats to passengers
+
+- Make sure your seat numbers do not have any spaces in them.
+- Here it might be good to reuse or call functions you have already defined.
+
+## 4. Ticket codes
+
+- You can use `len()` to get the length of a string.
+- You can use `"" * ` to repeat a string.
\ No newline at end of file
diff --git a/python/plane-tickets/README.md b/python/plane-tickets/README.md
new file mode 100644
index 00000000..7070fade
--- /dev/null
+++ b/python/plane-tickets/README.md
@@ -0,0 +1,270 @@
+# Plane Tickets
+
+Welcome to Plane Tickets on Exercism's Python Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
+
+## Introduction
+
+A `generator` is a function or expression that returns a special type of [iterator][iterator] called [generator iterator][generator-iterator].
+`Generator-iterators` are [lazy][lazy iterator]: they do not store their `values` in memory, but _generate_ their values when needed.
+
+A generator function looks like any other function, but contains one or more [yield expressions][yield expression].
+Each `yield` will suspend code execution, saving the current execution state (_including all local variables and try-statements_).
+When the generator resumes, it picks up state from the suspension - unlike regular functions which reset with every call.
+
+
+## Constructing a generator
+
+Generators are constructed much like other looping or recursive functions, but require a [`yield` expression](#the-yield-expression), which we will explore in depth a bit later.
+
+An example is a function that returns the _squares_ from a given list of numbers.
+As currently written, all input must be processed before any values can be returned:
+
+```python
+>>> def squares(list_of_numbers):
+... squares = []
+... for number in list_of_numbers:
+... squares.append(number ** 2)
+... return squares
+```
+
+You can convert that function into a generator like this:
+
+```python
+>>> def squares_generator(list_of_numbers):
+... for number in list_of_numbers:
+... yield number ** 2
+```
+
+The rationale behind this is that you use a generator when you do not need to produce all the values _at once_.
+This saves memory and processing power, since only the value you are _currently working on_ is calculated.
+
+
+## Using a generator
+
+Generators may be used in place of most `iterables` in Python.
+This includes _functions_ or _objects_ that require an `iterable`/`iterator` as an argument.
+
+To use the `squares_generator()` generator:
+
+```python
+>>> squared_numbers = squares_generator([1, 2, 3, 4])
+
+>>> for square in squared_numbers:
+... print(square)
+...
+1
+4
+9
+16
+```
+
+Values within a `generator` can also be produced/accessed via the `next()` function.
+`next()` calls the `__next__()` method of a generator-iterator object, "advancing" or evaluating the code up to its `yield` expression, which then "yields" or returns a value:
+
+```python
+>>> squared_numbers = squares_generator([1, 2])
+
+>>> next(squared_numbers)
+1
+>>> next(squared_numbers)
+4
+```
+
+When a `generator-iterator` is fully consumed and has no more values to return, it throws a `StopIteration` error.
+
+```python
+>>> next(squared_numbers)
+Traceback (most recent call last):
+ File "", line 1, in
+StopIteration
+```
+
+
+~~~~exercism/note
+
+Generator-iterators are a special sub-set of [iterators][iterator].
+`Iterators` are the mechanism/protocol that enables looping over _iterables_.
+Generator-iterators and the iterators returned by common Python [`iterables`][iterables] act very similarly, but there are some important differences to note:
+
+- They are _[lazily evaluated][lazy evaluation]_; iteration is _one-way_ and there is no "backing up" to a previous value.
+- They are _consumed_ by iterating over the returned values; there is no resetting or saving in memory.
+- They are not sortable and cannot be reversed.
+- They are not sequence types, and _do not_ have `indexes`.
+ You cannot reference a previous or future value using addition or subtraction and you cannot use bracket (`[]`) notation or slicing.
+- They cannot be used with the `len()` function, as they have no length.
+- They can be _finite_ or _infinite_ - be careful when collecting all values from an _infinite_ `generator-iterator`!
+
+[iterator]: https://docs.python.org/3.11/glossary.html#term-iterator
+[iterables]: https://wiki.python.org/moin/Iterator
+[lazy evaluation]: https://en.wikipedia.org/wiki/Lazy_evaluation
+~~~~
+
+
+## The yield expression
+
+The [yield expression][yield expression] is very similar to the `return` expression.
+_Unlike_ the `return` expression, `yield` gives up values to the caller at a _specific point_, suspending evaluation/return of any additional values until they are requested.
+When `yield` is evaluated, it pauses the execution of the enclosing function and returns any values of the function _at that point in time_.
+The function then _stays in scope_, and when `__next__()` is called, execution resumes until `yield` is encountered again.
+
+
+~~~~exercism/note
+Using `yield` expressions is prohibited outside of functions.
+~~~~
+
+```python
+>>> def infinite_sequence():
+... current_number = 0
+... while True:
+... yield current_number
+... current_number += 1
+
+>>> lets_try = infinite_sequence()
+>>> lets_try.__next__()
+0
+>>> lets_try.__next__()
+1
+```
+
+
+## Why Create a Generator?
+
+Generators are useful in a lot of applications.
+
+When working with a potentially large collection of values, you might not want to put all of them into memory.
+A generator can be used to work on larger data piece-by-piece, saving memory and improving performance.
+
+Generators are also very helpful when a process or calculation is _complex_, _expensive_, or _infinite_:
+
+```python
+>>> def infinite_sequence():
+... current_number = 0
+... while True:
+... yield current_number
+... current_number += 1
+```
+
+Now whenever `__next__()` is called on the `infinite_sequence` object, it will return the _previous number_ + 1.
+
+
+[generator-iterator]: https://docs.python.org/3.11/glossary.html#term-generator-iterator
+[iterables]: https://wiki.python.org/moin/Iterator
+[iterator]: https://docs.python.org/3.11/glossary.html#term-iterator
+[lazy evaluation]: https://en.wikipedia.org/wiki/Lazy_evaluation
+[lazy iterator]: https://en.wikipedia.org/wiki/Lazy_evaluation
+[yield expression]: https://docs.python.org/3.11/reference/expressions.html#yield-expressions
+
+## Instructions
+
+Conda Airlines is the programming-world's biggest airline, with over 10,000 flights a day!
+
+They are currently assigning all seats to passengers by hand; this will need to be automated.
+
+They have asked _you_ to create software to automate passenger seat assignments.
+They require your software to be memory efficient and performant.
+
+## 1. Generate seat letters
+
+Conda wants to generate seat letters for their airplanes.
+An airplane is made of rows of seats.
+Each row has _4 seats_.
+The seats in each row are always named `A`, `B`, `C`, and `D`.
+The first seat in the row is `A`, the second seat in the row is `B`, and so on.
+After reaching `D`, it should start again with `A`.
+
+Implement a function `generate_seat_letters()` that accepts an `int` that holds how many seat letters to be generated.
+The function should then return an _iterable_ of seat letters.
+
+```python
+>>> letters = generate_seat_letters(4)
+>>> next(letters)
+"A"
+>>> next(letters)
+"B"
+```
+
+## 2. Generate seats
+
+Conda wants a system that can generate a given number of seats for their airplanes.
+Each airplane has _4 seats_ in each row.
+The rows are defined using numbers, starting from `1` and going up.
+The seats should be ordered, like: `1A`, `1B`, `1C`, `1D`, `2A`, `2B`, `2C`, `2D`, `3A`, `3B`, `3C`, `3D`, ...
+
+Here is an example:
+
+| x | 1 | 2 |
+| :---------: | :-: | :-: |
+| Row | 5 | 21 |
+| Seat letter | A | D |
+| Result | 5A | 21D |
+
+Many airlines do not have _row_ number 13 on their flights, due to superstition amongst passengers.
+Conda Airlines also follows this convention, so make sure you _don't_ generate seats for _row_ number 13.
+
+Implement a function `generate_seats()` that accepts an `int` that holds how many seats to be generated.
+The function should then return an _iterable_ of seats given.
+
+```python
+>>> seats = generate_seats(10)
+>>> next(seats)
+"1A"
+>>> next(seats)
+"1B"
+```
+
+## 3. Assign seats to passengers
+
+Now that you have a function that generates seats, you can use it to assign seats to passengers.
+
+Implement a function `assign_seats()` that accepts a `list` of passenger names.
+The function should then return a _dictionary_ of `passenger` as _key_, and `seat_number` as _value_.
+
+```python
+>>> passengers = ['Jerimiah', 'Eric', 'Bethany', 'Byte', 'SqueekyBoots', 'Bob']
+
+>>> assign_seats(passengers)
+{'Jerimiah': '1A', 'Eric': '1B', 'Bethany': '1C', 'Byte': '1D', 'SqueekyBoots': '2A', 'Bob': '2B'}
+```
+
+## 4. Ticket codes
+
+Conda Airlines would like to have a unique code for each ticket.
+Since they are a big airline, they have a lot of flights.
+This means that there are multiple flights with the same seat number.
+They want you to create a system that creates a unique ticket that is _12_ characters long string code for identification.
+
+This code begins with the `assigned_seat` followed by the `flight_id`.
+The rest of the code is appended by `0s`.
+
+Implement a function `generate_codes(, )` that accepts a `list` of `seat_numbers` and a `string` with the flight number.
+The function should then return a `generator` that yields a `ticket_number`.
+
+```python
+>>> seat_numbers = ['1A', '17D']
+>>> flight_id = 'CO1234'
+>>> ticket_ids = generate_codes(seat_numbers, flight_id)
+
+>>> next(ticket_ids)
+'1ACO12340000'
+>>> next(ticket_ids)
+'17DCO1234000'
+```
+
+## Source
+
+### Created by
+
+- @J08K
+
+### Contributed to by
+
+- @BethanyG
+- @kytrinyx
+- @meatball133
+
+### My Solution
+
+- [my solution](./generators.py)
+- [run-tests](./run-tests-python.txt)
diff --git a/python/plane-tickets/generators.py b/python/plane-tickets/generators.py
new file mode 100644
index 00000000..aea5e48e
--- /dev/null
+++ b/python/plane-tickets/generators.py
@@ -0,0 +1,102 @@
+"""Functions to automate Conda airlines ticketing system."""
+
+from typing import Any, Generator
+
+IndexT = int
+QuantityT = int
+
+LetterT = str
+LettersT = list[LetterT]
+
+RowT = int
+SeatT = str
+SeatsT = list[SeatT]
+
+NameT = str
+PassengersT = list[NameT]
+SeatAssignmentsT = dict[NameT, SeatT]
+
+CodeT = str
+FlightIdT = str
+
+
+def generate_seat_letters(quantity: QuantityT) -> Generator[LetterT, Any, Any]:
+ """Generate a series of letters for airline seats.
+
+ :param quantity: int - total number of seat letters to be generated.
+ :return: generator - generator that yields seat letters.
+
+ Seat letters are generated from A to D.
+ After D it should start again with A.
+
+ Example: A, B, C, D
+ """
+
+ letters: LettersT = ["A", "B", "C", "D"]
+
+ for pos in range(0, quantity):
+ index: IndexT = pos % len(letters)
+
+ yield letters[index]
+
+
+def generate_seats(quantity: QuantityT) -> Generator[SeatT, Any, Any]:
+ """Generate a series of identifiers for airline seats.
+
+ :param quantity: int - total number of seats to be generated.
+ :return: generator - generator that yields seat numbers.
+
+ A seat number consists of the row number and the seat letter.
+
+ There is no row 13.
+ Each row has 4 seats.
+
+ Seats should be sorted from low to high.
+
+ Example: 3C, 3D, 4A, 4B
+ """
+
+ row: RowT = 0
+
+ for seat in generate_seat_letters(quantity):
+ if seat == "A":
+ row += 1
+
+ if row == 13:
+ row += 1
+
+ yield f"{row}{seat}"
+
+
+def assign_seats(passengers: PassengersT) -> SeatAssignmentsT:
+ """Assign seats to passengers.
+
+ :param passengers: list[str] - a list of strings containing names of passengers.
+ :return: dict - with the names of the passengers as keys and seat numbers as values.
+
+ Example output: {"Adele": "1A", "Björk": "1B"}
+ """
+
+ seat_assignments: SeatAssignmentsT = {}
+ seat: Generator[SeatT, Any, Any] = generate_seats(len(passengers))
+
+ for passenger in passengers:
+ seat_assignments[passenger] = next(seat)
+
+ return seat_assignments
+
+
+def generate_codes(
+ seat_numbers: SeatsT, flight_id: FlightIdT
+) -> Generator[CodeT, Any, Any]:
+ """Generate codes for a ticket.
+
+ :param seat_numbers: list[str] - list of seat numbers.
+ :param flight_id: str - string containing the flight identifier.
+ :return: generator - generator that yields 12 character long ticket codes.
+ """
+
+ for seat in seat_numbers:
+ code: CodeT = f"{seat}{flight_id}"
+
+ yield f"{code:012}"
diff --git a/python/plane-tickets/generators.py,cover b/python/plane-tickets/generators.py,cover
new file mode 100644
index 00000000..3cc7f56c
--- /dev/null
+++ b/python/plane-tickets/generators.py,cover
@@ -0,0 +1,102 @@
+> """Functions to automate Conda airlines ticketing system."""
+
+> from typing import Any, Generator
+
+> IndexT = int
+> QuantityT = int
+
+> LetterT = str
+> LettersT = list[LetterT]
+
+> RowT = int
+> SeatT = str
+> SeatsT = list[SeatT]
+
+> NameT = str
+> PassengersT = list[NameT]
+> SeatAssignmentsT = dict[NameT, SeatT]
+
+> CodeT = str
+> FlightIdT = str
+
+
+> def generate_seat_letters(quantity: QuantityT) -> Generator[LetterT, Any, Any]:
+> """Generate a series of letters for airline seats.
+
+> :param quantity: int - total number of seat letters to be generated.
+> :return: generator - generator that yields seat letters.
+
+> Seat letters are generated from A to D.
+> After D it should start again with A.
+
+> Example: A, B, C, D
+> """
+
+> letters: LettersT = ["A", "B", "C", "D"]
+
+> for pos in range(0, quantity):
+> index: IndexT = pos % len(letters)
+
+> yield letters[index]
+
+
+> def generate_seats(quantity: QuantityT) -> Generator[SeatT, Any, Any]:
+> """Generate a series of identifiers for airline seats.
+
+> :param quantity: int - total number of seats to be generated.
+> :return: generator - generator that yields seat numbers.
+
+> A seat number consists of the row number and the seat letter.
+
+> There is no row 13.
+> Each row has 4 seats.
+
+> Seats should be sorted from low to high.
+
+> Example: 3C, 3D, 4A, 4B
+> """
+
+> row: RowT = 0
+
+> for seat in generate_seat_letters(quantity):
+> if seat == "A":
+> row += 1
+
+> if row == 13:
+> row += 1
+
+> yield f"{row}{seat}"
+
+
+> def assign_seats(passengers: PassengersT) -> SeatAssignmentsT:
+> """Assign seats to passengers.
+
+> :param passengers: list[str] - a list of strings containing names of passengers.
+> :return: dict - with the names of the passengers as keys and seat numbers as values.
+
+> Example output: {"Adele": "1A", "Björk": "1B"}
+> """
+
+> seat_assignments: SeatAssignmentsT = {}
+> seat: Generator[SeatT, Any, Any] = generate_seats(len(passengers))
+
+> for passenger in passengers:
+> seat_assignments[passenger] = next(seat)
+
+> return seat_assignments
+
+
+> def generate_codes(
+> seat_numbers: SeatsT, flight_id: FlightIdT
+> ) -> Generator[CodeT, Any, Any]:
+> """Generate codes for a ticket.
+
+> :param seat_numbers: list[str] - list of seat numbers.
+> :param flight_id: str - string containing the flight identifier.
+> :return: generator - generator that yields 12 character long ticket codes.
+> """
+
+> for seat in seat_numbers:
+> code: CodeT = f"{seat}{flight_id}"
+
+> yield f"{code:012}"
diff --git a/python/plane-tickets/generators_test.py b/python/plane-tickets/generators_test.py
new file mode 100644
index 00000000..12a3532a
--- /dev/null
+++ b/python/plane-tickets/generators_test.py
@@ -0,0 +1,137 @@
+import inspect
+import unittest
+import pytest
+
+from generators import (
+ generate_seat_letters,
+ generate_seats,
+ assign_seats,
+ generate_codes
+)
+
+class PlaneTicketsTest(unittest.TestCase):
+ @pytest.mark.task(taskno=1)
+ def test_task1_returns_generator(self):
+ """Test if generate_seat_letters() returns a generator type."""
+
+ number = 5
+ error_message = (f'Called generate_seat_letters({number}). '
+ f'The function returned a {type(generate_seat_letters(number))} type, '
+ f"but the tests expected the function to return a type.")
+
+ self.assertTrue(inspect.isgenerator(generate_seat_letters(number)), msg=error_message)
+
+ @pytest.mark.task(taskno=1)
+ def test_generate_seat_letters(self):
+ test_data = [1, 2, 3, 4, 5]
+ result_data = [["A"],
+ ["A", "B"],
+ ["A", "B", "C"],
+ ["A", "B", "C", "D"],
+ ["A", "B", "C", "D", "A"]]
+
+ for variant, (number, expected) in enumerate(zip(test_data, result_data), start=1):
+ with self.subTest(f"variation #{variant}", number=number, expected=expected):
+ actual_result = list(generate_seat_letters(number))
+ error_message = (f'Called generate_seat_letters({number}). '
+ f'The function returned {actual_result}, but the tests '
+ f'expected {expected} when generating {number} seat(s).')
+
+ self.assertEqual(actual_result, expected, msg=error_message)
+
+ @pytest.mark.task(taskno=2)
+ def test_task2_returns_generator(self):
+ """Test if generate_seats() returns a generator type."""
+
+ number = 7
+ error_message = (f'Called generate_seats({number}). '
+ f'The function returned a {type(generate_seats(number))} type, '
+ f"but the tests expected the function to return a type.")
+
+ self.assertTrue(inspect.isgenerator(generate_seats(number)), msg=error_message)
+
+ @pytest.mark.task(taskno=2)
+ def test_generate_seats(self):
+ test_data = [1, 2, 3, 4, 5]
+ result_data = [["1A"],
+ ["1A", "1B"],
+ ["1A", "1B", "1C"],
+ ["1A", "1B", "1C", "1D"],
+ ["1A", "1B", "1C", "1D", "2A"]]
+
+ for variant, (number, expected) in enumerate(zip(test_data, result_data), start=1):
+ with self.subTest(f"variation #{variant}", number=number, expected=expected):
+ actual_result = list(generate_seats(number))
+ error_message = (f'Called generate_seats({number}). '
+ f'The function returned {actual_result}, but the tests '
+ f'expected {expected} when generating {number} seat(s).')
+
+ self.assertEqual(actual_result, expected, msg=error_message)
+
+ @pytest.mark.task(taskno=2)
+ def test_generate_seats_skips_row_13(self):
+ test_data = [14 * 4]
+ result_data = [["1A", "1B", "1C", "1D", "2A", "2B", "2C", "2D",
+ "3A", "3B", "3C", "3D", "4A", "4B", "4C", "4D",
+ "5A", "5B", "5C", "5D", "6A", "6B", "6C", "6D",
+ "7A", "7B", "7C", "7D", "8A", "8B", "8C", "8D",
+ "9A", "9B", "9C", "9D", "10A", "10B", "10C", "10D",
+ "11A", "11B", "11C", "11D", "12A", "12B", "12C", "12D",
+ "14A", "14B", "14C", "14D", "15A", "15B", "15C", "15D"]]
+
+ for variant, (number, expected) in enumerate(zip(test_data, result_data), start=1):
+ with self.subTest(f"variation #{variant}", number=number, expected=expected):
+ actual_result = list(generate_seats(number))
+ error_message = (f'Called generate_seats({number}). '
+ f'The function returned {actual_result}, but the tests '
+ f'expected: {expected}, when generating {number} seat(s).')
+
+ self.assertEqual(actual_result, expected, msg=error_message)
+
+ @pytest.mark.task(taskno=3)
+ def test_assign_seats(self):
+ test_data = [["Passenger1", "Passenger2", "Passenger3", "Passenger4", "Passenger5"],
+ ["TicketNo=5644", "TicketNo=2273", "TicketNo=493", "TicketNo=5411", "TicketNo=824"]]
+ result_data = [{"Passenger1": "1A", "Passenger2": "1B",
+ "Passenger3": "1C", "Passenger4": "1D", "Passenger5": "2A"},
+ {"TicketNo=5644": "1A", "TicketNo=2273": "1B",
+ "TicketNo=493": "1C", "TicketNo=5411": "1D", "TicketNo=824": "2A"}]
+
+ for variant, (passengers, expected) in enumerate(zip(test_data, result_data), start=1):
+ with self.subTest(f"variation #{variant}", passengers=passengers, expected=expected):
+ actual_result = assign_seats(passengers)
+ error_message = (f'Called assign_seats({passengers}). '
+ f'The function returned {actual_result}, but the tests '
+ f'expected {expected}, when assigning seats.')
+
+ self.assertEqual(actual_result, expected, msg=error_message)
+
+ @pytest.mark.task(taskno=4)
+ def test_task4_returns_generator(self):
+ """Test if generate_codes() returns a generator type."""
+
+ seat_numbers, flight_id = "11B", "HA80085"
+ error_message = (f'Called generate_codes({seat_numbers}, {flight_id}). '
+ f'The function returned a {type(generate_codes(seat_numbers, flight_id))} type, '
+ f"but the tests expected the function to return a type.")
+
+ self.assertTrue(inspect.isgenerator(generate_codes(seat_numbers, flight_id)), msg=error_message)
+
+
+ @pytest.mark.task(taskno=4)
+ def test_generate_codes(self):
+ test_data = [(["12A", "38B", "69C", "102B"],"KL1022"),
+ (["22C", "88B", "33A", "44B"], "DL1002")]
+ result_data = [['12AKL1022000', '38BKL1022000', '69CKL1022000', '102BKL102200'],
+ ['22CDL1002000', '88BDL1002000', '33ADL1002000', '44BDL1002000']]
+
+ for variant, ((seat_numbers, flight_id), expected) in enumerate(zip(test_data, result_data), start=1):
+ with self.subTest(f"variation #{variant}", seat_numbbers=seat_numbers,
+ flight_id=flight_id, expected=expected):
+
+ actual_result = list(generate_codes(seat_numbers, flight_id))
+ error_message = (f'Called generate_codes({seat_numbers}, {flight_id}). '
+ f'The function returned {actual_result}, but the tests '
+ f'expected {expected} when generating ticket numbers.')
+
+ self.assertEqual(list(generate_codes(seat_numbers, flight_id)), expected, msg=error_message)
diff --git a/python/plane-tickets/pytest.ini b/python/plane-tickets/pytest.ini
new file mode 100644
index 00000000..8dc12a49
--- /dev/null
+++ b/python/plane-tickets/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+pythonpath = .
+addopts = --doctest-modules
+markers =
+ task: exercise task/step
diff --git a/python/plane-tickets/run-tests-python.txt b/python/plane-tickets/run-tests-python.txt
new file mode 100644
index 00000000..96f7eb91
--- /dev/null
+++ b/python/plane-tickets/run-tests-python.txt
@@ -0,0 +1,500 @@
+Running automated test file(s):
+
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-lint-pylint
+
+Running Python Lint - PyLint
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+Running: pylint --version
+
+pylint 3.0.3
+astroid 3.0.2
+Python 3.12.1 (main, Dec 28 2023, 08:22:05) [GCC 10.2.1 20210110]
+
+real 0m0.238s
+user 0m0.158s
+sys 0m0.082s
+
+
+ ==============================================================================
+
+Running: pylint ./src
+
+
+-------------------------------------------------------------------
+Your code has been rated at 10.00/10 (previous run: 9.74/10, +0.26)
+
+
+real 0m0.768s
+user 0m0.685s
+sys 0m0.084s
+
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m1.992s
+user 0m1.511s
+sys 0m0.501s
+
+real 0m1.997s
+user 0m1.512s
+sys 0m0.505s
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-lint-ruff
+
+Running Python Lint - Ruff
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+Running: ruff --version
+
+ruff 0.3.5
+
+real 0m0.070s
+user 0m0.032s
+sys 0m0.039s
+
+
+ ==============================================================================
+
+Running: ruff check --ignore E501 ./src
+
+All checks passed!
+
+real 0m0.126s
+user 0m0.055s
+sys 0m0.074s
+
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m1.149s
+user 0m0.754s
+sys 0m0.415s
+
+real 0m1.151s
+user 0m0.756s
+sys 0m0.415s
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-lint-pyright
+
+Running Python Lint - PyRight
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+Running: pyright --version
+
+pyright 1.1.357
+
+real 0m0.676s
+user 0m0.500s
+sys 0m0.128s
+
+
+ ==============================================================================
+
+Running: pyright --stats ./src
+
+Found 2 source files
+pyright 1.1.357
+0 errors, 0 warnings, 0 informations
+Completed in 0.601sec
+
+Analysis stats
+Total files parsed and bound: 20
+Total files checked: 2
+
+Timing stats
+Find Source Files: 0sec
+Read Source Files: 0sec
+Tokenize: 0.06sec
+Parse: 0.05sec
+Resolve Imports: 0.05sec
+Bind: 0.06sec
+Check: 0.06sec
+Detect Cycles: 0sec
+
+real 0m1.269s
+user 0m1.421s
+sys 0m0.188s
+
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m2.930s
+user 0m2.579s
+sys 0m0.658s
+
+real 0m2.933s
+user 0m2.581s
+sys 0m0.659s
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-lint-bandit
+
+Running Python Lint - Bandit
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+Running: bandit --version
+
+bandit 1.7.7
+ python version = 3.12.1 (main, Dec 28 2023, 08:22:05) [GCC 10.2.1 20210110]
+
+real 0m0.285s
+user 0m0.220s
+sys 0m0.067s
+
+
+ ==============================================================================
+
+Running: bandit --verbose --recursive ./src
+
+[main] INFO profile include tests: None
+[main] INFO profile exclude tests: None
+[main] INFO cli include tests: None
+[main] INFO cli exclude tests: None
+[main] INFO running on Python 3.12.1
+Run started:2024-04-05 03:39:46.246487
+Files in scope (2):
+ ./src/plane_tickets/__init__.py (score: {SEVERITY: 0, CONFIDENCE: 0})
+ ./src/plane_tickets/generators.py (score: {SEVERITY: 0, CONFIDENCE: 0})
+Files excluded (2):
+ ./src/plane_tickets/__pycache__/__init__.cpython-312.pyc
+ ./src/plane_tickets/__pycache__/generators.cpython-312.pyc
+
+Test results:
+ No issues identified.
+
+Code scanned:
+ Total lines of code: 65
+ Total lines skipped (#nosec): 0
+ Total potential issues skipped due to specifically being disabled (e.g., #nosec BXXX): 0
+
+Run metrics:
+ Total issues (by severity):
+ Undefined: 0
+ Low: 0
+ Medium: 0
+ High: 0
+ Total issues (by confidence):
+ Undefined: 0
+ Low: 0
+ Medium: 0
+ High: 0
+Files skipped (0):
+
+real 0m0.237s
+user 0m0.166s
+sys 0m0.071s
+
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m1.526s
+user 0m1.063s
+sys 0m0.482s
+
+real 0m1.529s
+user 0m1.064s
+sys 0m0.484s
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-lint-refurb
+
+Running Python Lint - Refurb
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+Running: refurb --version
+
+Refurb: v1.26.0
+Mypy: v1.9.0
+
+real 0m0.200s
+user 0m0.143s
+sys 0m0.058s
+
+
+ ==============================================================================
+
+Running: refurb ./src
+
+
+real 0m1.110s
+user 0m1.029s
+sys 0m0.082s
+
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m2.249s
+user 0m1.830s
+sys 0m0.434s
+
+real 0m2.252s
+user 0m1.830s
+sys 0m0.436s
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-test-with-doctests
+
+Running Python DocTests
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+PYTHONPATH="./src"
+
+ ==============================================================================
+
+Running: python -m doctest -v ./src/plane_tickets/generators.py ./src/plane_tickets/__init__.py
+
+5 items had no tests:
+ generators
+ generators.assign_seats
+ generators.generate_codes
+ generators.generate_seat_letters
+ generators.generate_seats
+0 tests in 5 items.
+0 passed and 0 failed.
+Test passed.
+1 items had no tests:
+ __init__
+0 tests in 1 items.
+0 passed and 0 failed.
+Test passed.
+
+real 0m0.146s
+user 0m0.087s
+sys 0m0.061s
+
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m1.088s
+user 0m0.759s
+sys 0m0.344s
+
+real 0m1.090s
+user 0m0.759s
+sys 0m0.347s
+
+===============================================================================
+
+Running: ../../.github/citools/python/python-test-with-coverage
+
+Running Python Tests With Coverage
+
+Python versions:
+
+ Python 3.12.1
+ pyenv 2.3.36-21-g7e550e31
+ pip 24.0 from /home/vpayno/.pyenv/versions/3.12.1/lib/python3.12/site-packages/pip (python 3.12)
+ PDM, version 2.13.2
+
+
+ ==============================================================================
+
+Running: rm -rf ./coverage
+
+
+real 0m0.001s
+user 0m0.000s
+sys 0m0.001s
+
+
+ ==============================================================================
+
+Running: pytest --version
+
+pytest 7.4.3
+
+real 0m0.862s
+user 0m0.914s
+sys 0m0.836s
+
+
+ ==============================================================================
+
+PYTHONPATH="./src"
+
+ ==============================================================================
+
+Running: pytest --verbose --cov=. --cov-branch --cov-report=term-missing --cov-report=xml:.coverage.xml -p no:randomly ./test
+
+============================= test session starts ==============================
+platform linux -- Python 3.12.1, pytest-7.4.3, pluggy-1.3.0 -- /home/vpayno/.pyenv/versions/3.12.1/bin/python
+cachedir: .pytest_cache
+hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/vpayno/git_vpayno/exercism-workspace/python/plane-tickets/.hypothesis/examples'))
+rootdir: /home/vpayno/git_vpayno/exercism-workspace/python/plane-tickets
+configfile: pytest.ini
+plugins: anyio-4.2.0, libtmux-0.25.0, pylama-8.4.1, cov-4.1.0, datafiles-3.0.0, docker-2.0.1, subprocess-1.5.0, typeguard-4.1.5, hypothesis-6.98.17, snapshot-0.9.0
+collecting ... collected 8 items
+
+test/generators_test.py::PlaneTicketsTest::test_assign_seats PASSED [ 12%]
+test/generators_test.py::PlaneTicketsTest::test_generate_codes PASSED [ 25%]
+test/generators_test.py::PlaneTicketsTest::test_generate_seat_letters PASSED [ 37%]
+test/generators_test.py::PlaneTicketsTest::test_generate_seats PASSED [ 50%]
+test/generators_test.py::PlaneTicketsTest::test_generate_seats_skips_row_13 PASSED [ 62%]
+test/generators_test.py::PlaneTicketsTest::test_task1_returns_generator PASSED [ 75%]
+test/generators_test.py::PlaneTicketsTest::test_task2_returns_generator PASSED [ 87%]
+test/generators_test.py::PlaneTicketsTest::test_task4_returns_generator PASSED [100%]
+
+---------- coverage: platform linux, python 3.12.1-final-0 -----------
+Name Stmts Miss Branch BrPart Cover Missing
+-----------------------------------------------------------
+generators.py 36 0 12 0 100%
+-----------------------------------------------------------
+TOTAL 36 0 12 0 100%
+Coverage XML written to file .coverage.xml
+
+
+============================== 8 passed in 0.37s ===============================
+
+real 0m1.209s
+user 0m1.049s
+sys 0m0.161s
+
+
+ ==============================================================================
+
+Running: coverage report --show-missing
+
+Name Stmts Miss Branch BrPart Cover Missing
+-----------------------------------------------------------
+generators.py 36 0 12 0 100%
+-----------------------------------------------------------
+TOTAL 36 0 12 0 100%
+
+real 0m0.172s
+user 0m0.105s
+sys 0m0.069s
+
+
+ ==============================================================================
+
+Running: coverage annotate
+
+
+real 0m0.162s
+user 0m0.102s
+sys 0m0.061s
+
+
+ ==============================================================================
+
+Line Coverage: 100.0%
+Branch Coverage: 100.0%
+
+ ==============================================================================
+
+Exit code: 0
+
+real 0m3.377s
+user 0m2.849s
+sys 0m1.437s
+
+real 0m3.379s
+user 0m2.850s
+sys 0m1.437s
+
+===============================================================================
+
+tail -n 10000 ./*,cover | grep -E -C 3 '^> def |^! '
+
+> def generate_seat_letters(quantity: QuantityT) -> Generator[LetterT, Any, Any]:
+> """Generate a series of letters for airline seats.
+--
+
+> def generate_seats(quantity: QuantityT) -> Generator[SeatT, Any, Any]:
+> """Generate a series of identifiers for airline seats.
+--
+
+> def assign_seats(passengers: PassengersT) -> SeatAssignmentsT:
+> """Assign seats to passengers.
+--
+
+> def generate_codes(
+> seat_numbers: SeatsT, flight_id: FlightIdT
+
+===============================================================================
+
+Running: misspell ./src/plane_tickets/generators.py ./src/plane_tickets/__init__.py
+
+real 0m0.020s
+user 0m0.021s
+sys 0m0.008s
+
+===============================================================================
+
diff --git a/python/plane-tickets/src/plane_tickets/__init__.py b/python/plane-tickets/src/plane_tickets/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/python/plane-tickets/src/plane_tickets/generators.py b/python/plane-tickets/src/plane_tickets/generators.py
new file mode 120000
index 00000000..7bd19289
--- /dev/null
+++ b/python/plane-tickets/src/plane_tickets/generators.py
@@ -0,0 +1 @@
+../../generators.py
\ No newline at end of file
diff --git a/python/plane-tickets/test/__init__.py b/python/plane-tickets/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/python/plane-tickets/test/generators_test.py b/python/plane-tickets/test/generators_test.py
new file mode 120000
index 00000000..bf11ad55
--- /dev/null
+++ b/python/plane-tickets/test/generators_test.py
@@ -0,0 +1 @@
+../generators_test.py
\ No newline at end of file