A Dirty Python Script to Write User Stories Quickly
Tags: python, tooling, beginners
Published: 2023-07-05 10:09:33

I am not a product manager but I dabble. At work we've recently started an initiative that really needs some proper user story mapping (in my humble opinion). We're a fully distributed team and our goto for this kind of thing is Miro.

Because of the nature of this project, I have a lot of documentation to comb through and lot of user story cards to create and organize. The problem is, __I'm too impatient to create them directly in Miro__ with all that pointing and clicking and rewriting.

I know google sheets can be copied to Miro boards and the data will be imported as sticky notes. I also know that google sheets can be uploaded as CSV files.

Here is the idea: a script for quickly entering user stories and exporting those user stories as a CSV file that can be uploaded to Google Sheets. I have reasons to spend more time with Python, so that is what I'm going to write this script in.

I'm going to build this script progressively.

## Collecting input

Let's start by collecting input.

```python
role = input("As a(n): ")
function = input("I want to: ")
outcome = input("So that: ")

print(f"As a(n) {role} I want to {function} so that {outcome}")
```

Easy enough. Python gives us the `input()` method and these nice `f-strings` for string interpolation.

Run the script and you can input your information but the script will terminate as soon as you've typed in your first story. That is no good because I want to input many stories. So after I'm done completing one story, I want it to prompt me again. I'll add a while loop:

```python
while True:
	role = input("As a(n): ")
	function = input("I want to: ")
	outcome = input("So that: ")
	print(f"As a(n) {role} I want to {function} so that {outcome}\n\n")
```

Now we're looping forever (or until you press Ctrl-C). I've also added two line breaks at the end of the last line so there is some space between stories. The only issue is that the formatting is a little funky. I'd like it to clear the screen after each story so the console does not get crowded:

```python
import os

os.system("clear")

while True:
	role = input("As a(n): ")
	function = input("I want to: ")
	outcome = input("So that: ")
	os.system("clear")
	print(f"As a(n) {role} I want to {function} so that {outcome}\n\n")
```

We import the `os` module and send the `clear` command to the console after each story is input.

## Writing the stories to a CSV file

So how do we write to a file in python?

```python
import os

os.system("clear")

file = open("stories.csv", "w")

try:
	while True:
		role = input("As a(n): ")
		function = input("I want to: ")
		outcome = input("So that: ")
		os.system("clear")
		file.write(f"As a(n) {role} I want to {function} so that {outcome}\n")
except KeyboardInterrupt:
	file.close()
	os.system("clear")
	print("g'bye")
```

Python gives us the `open()` function which takes the name of the file to write to and the mode to open the file with. (In this case "w" for write.) We need to make sure to close the file when we're done with it so I added a try/except block. By catching the `KeyboardInterrupt` function in the except block, I can close the file when Ctrl-C is pressed.

## Testing and Loose Ends

I just ran a simple manual test: run the script, upload the resulting CSV file to google sheets and copy-paste the cells from that sheet to Miro. It works like a charm.

The last thing I want to do is clean up the hardcoded file name so the user can change it if they'd like:

```python
import os

os.system("clear")

file_path = input("Path to stories csv (./stories.csv): ")
file_path = "stories.csv" if file_path == "" else file_path

print(f"Writing stories to {file_path}\n\n")

file = open(file_path, "w")

try:

	while True:
		role = input("As a(n): ")
		function = input("I want to: ")
		outcome = input("So that: ")
		os.system("clear")
		file.write(f"As a(n) {role} I want to {function} so that {outcome}\n")
except KeyboardInterrupt:
	file.close()
	os.system("clear")
	print("g'bye")
```

I've prompted the user for a file name and provided a default.

Since we're accepting user input for the filename, the user can give us a bad filename and this would cause the script to output a terrible looking error. Since I'm the only user of this script, I normally wouldn't mind too much. But since I'm also  getting comfortable with Python, let's make it nicer.

To do that, I need to figure out what error Python will give me if we provide a bad filename. Try giving it a filename like "~/blah blah %$#@ no" and see what happens. We get a `FileNotFoundError`.

What do we want to do when we get this error? We can force the default filename, or we can end the script with a friendly message. I like the last option so I don't accidentally write over a file that maybe I wanted to keep around.

```python
#!/usr/bin/env python3
import os

os.system("clear")

file_path = input("Path to stories csv (./stories.csv): ")
file_path = "stories.csv" if file_path == "" else file_path

try:
    file = open(file_path, "w")
    print(f"Writing stories to {file_path}\n\n")

    while True:
        role = input("As a(n): ")
        function = input("I want to: ")
        outcome = input("So that: ")
        os.system("clear")
        file.write(f"As a(n) {role} I want to {function} so that {outcome}\n")
except KeyboardInterrupt:
    file.close()
    os.system('clear')
    print("g'bye")
except FileNotFoundError:
    print(f"{file_path} is not a valid filename.")

```

I've added a new `except` block and printed a friendly message. I've also added a shebang to the beginning of the script so I can put it in my bin folder and run it from anywhere. This now lives in [my dotfiles repo](https://github.com/ncko/.dotfiles/blob/main/bin/.local/bin/user-stories) until I decide to get rid of it.

## Nothing else to see here

Quick and dirty tools are some of my favorite things about coding and Python is a great language to create them in.