Skip to content

Commit

Permalink
Setup improvements (#394)
Browse files Browse the repository at this point in the history
Closes #393; updates #281 

- Run db setup if the app was just updated to a different version
- Split CLI setup commands into 'setup shell' and 'setup db'
- Add 'setup db --download' CLI option
- Start indexing observation text in FTS table
- Persist app state in SQLite instead of yaml

Minor housekeeping:
- Move db setup functions to a separate module
- Move client, settings, and setup modules to separate 'storage'
subpackage
- Make data dir link on About dialog clickable
- Disable 'view observations' button for now
  • Loading branch information
JWCook authored Jul 2, 2024
2 parents c566795 + b889b26 commit a10b563
Show file tree
Hide file tree
Showing 20 changed files with 664 additions and 551 deletions.
2 changes: 1 addition & 1 deletion HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* Split CLI into subcommands:
* `tag`: Main tagging features
* `refresh`: Refresh tags for previously tagged images
* `install`: install shell completion + taxonomy database
* `setup`: install shell completion + taxonomy database
* Add 3 verbosity levels (`nt -v[vv]`)
* Update `nt tag -p` to accept directories and glob patterns
* Add support for alternate XMP sidecar path format, if it already exists
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,17 @@ See [Application Guide](https://naturtag.readthedocs.io/en/stable/app.html) for

### CLI
Naturtag also includes a command-line interface. It takes an observation or species, plus some image
files, and generates EXIF and XMP metadata to write to those images. You can see it in action here:
files, and generates EXIF and XMP metadata to write to those images.

Example:
```bash
# Tag images with metadata from observation ID 5432
nt tag -o 5432 img1.jpg img2.jpg

# Refresh previously tagged images with latest observation and taxonomy metadata
nt refresh -r ~/observations
```
You can see it in action here:
[![asciicast](https://asciinema.org/a/0a6gzpt7AI9QpGoq0OGMDOxqi.svg)](https://asciinema.org/a/0a6gzpt7AI9QpGoq0OGMDOxqi)

See [CLI documentation](https://naturtag.readthedocs.io/en/stable/cli.html) for more details.
Expand All @@ -108,7 +118,7 @@ applications. Basic example:
from naturtag import tag_images, refresh_tags

# Tag images with full observation metadata
tag_images(['img1.jpg', 'img2.jpg'], observation_id=1234)
tag_images(['img1.jpg', 'img2.jpg'], observation_id=5432)

# Refresh previously tagged images with latest observation and taxonomy metadata
refresh_tags(['~/observations/'], recursive=True)
Expand Down
46 changes: 34 additions & 12 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,51 @@ It takes an observation or species, plus some image files, and generates EXIF an
write to those images. You can see it in action here:
[![asciicast](https://asciinema.org/a/0a6gzpt7AI9QpGoq0OGMDOxqi.svg)](https://asciinema.org/a/0a6gzpt7AI9QpGoq0OGMDOxqi)

```{note}
See `nt <command> --help` for full usage information of any command.
## General usage
* Help: See `nt <command> --help` for full usage information of any command.
* Output verbosity: Run `nt -v[vv]` for more verbose debug output.

## Setup
The `setup` command group can run first-time setup steps and set up optional features of naturtag.

### Database
The `setup db` command sets up Naturtag's local database.

Naturtag uses a SQLite database to store observation and taxonomy data. This command can
initialize it for the first time, reset it, or download missing data for taxon text search.

Options:
```bash
-d, --download Download taxonomy data if it does not exist locally
-f, --force Reset database if it already exists
```
Example: Full reset and download, with debug logs:
```bash
nt -vv setup db -f -d
```
## Install
The `install` command sets up optional shell completion and other features.
### Shell
The `setup shell` command sets up optional shell tab-completion.
Options:
```bash
-s, --shell [bash|fish] Install completion script for a specific shell only
```
Shell tab-completion is available for bash and fish shells. To install, run:
```bash
nt install -s [shell name]
nt setup shell
```
This will provide tab-completion for CLI options as well as taxon names, for example:
Or for a specific shell only:
```bash
nt tag -t corm<TAB>
nt setup shell -s [shell name]
```
Options:
This will provide tab-completion for CLI options as well as taxon names, for example:
```bash
-a, --all Install all features
-d, --db Initialize taxonomy database
-s, --shell [bash|fish] Install shell completion scripts
-f, --force Reset database if it already exists
nt tag -t corm<TAB>
```
## Tag
Expand Down
25 changes: 13 additions & 12 deletions naturtag/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@
from naturtag.app.settings_menu import SettingsMenu
from naturtag.app.style import fa_icon, set_theme
from naturtag.app.threadpool import ThreadPool
from naturtag.client import ImageSession, iNatDbClient
from naturtag.constants import APP_ICON, APP_LOGO, ASSETS_DIR, DOCS_URL, REPO_URL
from naturtag.controllers import ImageController, ObservationController, TaxonController
from naturtag.settings import Settings, setup
from naturtag.storage import ImageSession, Settings, iNatDbClient, setup
from naturtag.widgets import VerticalLayout, init_handler

# Provide an application group so Windows doesn't use the default 'python' icon
Expand All @@ -48,20 +47,21 @@ def __init__(self, *args, **kwargs):
self.setApplicationVersion(pkg_version('naturtag'))
self.setOrganizationName('pyinat')
self.setWindowIcon(QIcon(QPixmap(str(APP_ICON))))
self.settings = Settings.read()

def post_init(self):
# Run any first-time setup steps, if needed
setup(self.settings)

# Globally available application objects
self.client = iNatDbClient(self.settings.db_path)
self.img_session = ImageSession(self.settings.image_cache_path)
self.settings = Settings.read()
self.log_handler = init_handler(
self.settings.log_level,
root_level=self.settings.log_level_external,
logfile=self.settings.logfile,
)

# Run initial/post-update setup steps, if needed
self.state = setup(self.settings.db_path)

# Globally available application objects
self.client = iNatDbClient(self.settings.db_path)
self.img_session = ImageSession(self.settings.image_cache_path)
self.threadpool = ThreadPool(n_worker_threads=self.settings.n_worker_threads)
self.user_dirs = UserDirs(self.settings)

Expand All @@ -70,7 +70,7 @@ class MainWindow(QMainWindow):
def __init__(self, app: NaturtagApp):
super().__init__()
self.setWindowTitle('Naturtag')
self.resize(*app.settings.window_size)
self.resize(*app.state.window_size)
self.app = app

# Controllers
Expand Down Expand Up @@ -210,7 +210,7 @@ def check_username(self):
def closeEvent(self, _):
"""Save settings before closing the app"""
self.app.settings.write()
self.taxon_controller.user_taxa.write()
self.app.state.write()

def info(self, message: str):
"""Show a message both in the status bar and in the logs"""
Expand Down Expand Up @@ -238,7 +238,8 @@ def open_about(self):
repo_link = f"<a href='{REPO_URL}'>{REPO_URL}</a>"
license_link = f"<a href='{REPO_URL}/LICENSE'>MIT License</a>"
attribution = f'Ⓒ {datetime.now().year} Jordan Cook, {license_link}'
app_dir_link = f"<a href='{self.app.settings.data_dir}'>{self.app.settings.data_dir}</a>"
data_dir = self.app.settings.data_dir
app_dir_link = f"<a href='file://{data_dir}'>{data_dir}</a>"

about.setText(
f'<b>Naturtag v{version}</b><br/>'
Expand Down
2 changes: 1 addition & 1 deletion naturtag/app/controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from PySide6.QtWidgets import QApplication, QFileDialog, QMenu, QSizePolicy, QToolBar, QWidget

from naturtag.app.style import fa_icon
from naturtag.settings import Settings
from naturtag.storage import Settings

HOME_DIR = str(Path.home())
logger = getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion naturtag/app/settings_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)

from naturtag.controllers import BaseController
from naturtag.settings import Settings
from naturtag.storage import Settings
from naturtag.utils import read_locales
from naturtag.widgets import FAIcon, HorizontalLayout, ToggleSwitch, VerticalLayout

Expand Down
69 changes: 45 additions & 24 deletions naturtag/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from naturtag.constants import CLI_COMPLETE_DIR
from naturtag.metadata import KeywordMetadata, MetaMetadata, refresh_tags, tag_images
from naturtag.settings import Settings, setup
from naturtag.storage import Settings, setup
from naturtag.utils import get_valid_image_paths, strip_url

CODE_BLOCK = re.compile(r'```\n\s*(.+?)```\n', re.DOTALL)
Expand Down Expand Up @@ -209,29 +209,59 @@ def refresh(recursive, image_paths):
click.echo(f'{len(metadata_objs)} Images refreshed')


@main.command()
@click.pass_context
@click.option('-a', '--all', is_flag=True, help='Install all features')
@click.option('-d', '--db', is_flag=True, help='Initialize taxonomy database')
@main.group(name='setup')
def setup_group():
"""Setup commands"""


@setup_group.command()
@click.option(
'-s',
'--shell',
type=click.Choice(['bash', 'fish']),
help='Install shell completion scripts',
'-d',
'--download',
is_flag=True,
help='Download taxonomy data if it does not exist locally',
)
@click.option(
'-f',
'--force',
is_flag=True,
help='Reset database if it already exists',
)
def install(ctx, all, db, shell, force):
"""Install shell completion and other features.
def db(download, force):
"""Set up Naturtag's local database.
Naturtag uses a SQLite database to store observation and taxonomy data. This command can
initialize it for the first time, reset it, or download missing data for taxon text search.
\b
Shell tab-completion is available for bash and fish shells. To install, run:
Example: Full reset and download, with debug logs:
```
nt install -s [shell name]
nt -vv setup db -f -d
```
"""
click.echo('Initializing database...')
setup(overwrite=force, download=download)


@setup_group.command()
@click.option(
'-s',
'--shell',
type=click.Choice(['bash', 'fish']),
help='Install completion script for a specific shell only',
)
def shell(shell):
"""Install shell tab-completion for naturtag.
\b
Completion is available for bash and fish shells. To install, run:
```
nt setup shell
```
Or for a specific shell only:
```
nt setup shell -s [shell name]
```
\b
Expand All @@ -240,16 +270,7 @@ def install(ctx, all, db, shell, force):
nt tag -t corm<TAB>
```
"""
if not any([all, db, shell]):
click.echo('Specify at least one feature to install')
click.echo(ctx.get_help())
ctx.exit()

if all or shell:
install_shell_completion('all' if all else shell)
if all or db:
click.echo('Initializing database...')
setup(overwrite=force)
install_shell_completion(shell or 'all')


def enable_logging(level: str = 'INFO', external_level: str = 'WARNING'):
Expand Down Expand Up @@ -393,5 +414,5 @@ def _install_bash_completion():
print(f'source {completion_dir}/*.bash\n')


for cmd in [tag, refresh, install]:
for cmd in [tag, refresh, db, shell]:
cmd.help = colorize_help_text(cmd.help)
4 changes: 2 additions & 2 deletions naturtag/controllers/observation_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ def get_user_observations(self) -> list[Observation]:

observations = self.app.client.observations.get_user_observations(
username=self.app.settings.username,
updated_since=self.app.settings.last_obs_check,
updated_since=self.app.state.last_obs_check,
limit=DEFAULT_PAGE_SIZE,
page=self.page,
)
self.app.settings.set_obs_checkpoint()
self.app.state.set_obs_checkpoint()
return observations
7 changes: 4 additions & 3 deletions naturtag/controllers/taxon_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
TaxonSearch,
get_app,
)
from naturtag.settings import UserTaxa
from naturtag.storage import AppState
from naturtag.widgets import HorizontalLayout, TaxonInfoCard, TaxonList, VerticalLayout

logger = getLogger(__name__)
Expand All @@ -27,7 +27,7 @@ class TaxonController(BaseController):

def __init__(self):
super().__init__()
self.user_taxa = UserTaxa.read(self.app.settings.user_taxa_path)
self.user_taxa = self.app.state

self.root = HorizontalLayout(self)
self.root.setAlignment(Qt.AlignLeft)
Expand Down Expand Up @@ -118,7 +118,7 @@ class TaxonTabs(QTabWidget):

def __init__(
self,
user_taxa: UserTaxa,
user_taxa: AppState,
parent: Optional[QWidget] = None,
):
super().__init__(parent)
Expand Down Expand Up @@ -222,6 +222,7 @@ def display_observed(self, taxon_counts: TaxonCounts):
"""After fetching observation taxon counts for the user, add info cards for them"""
self.observed.set_taxa(list(taxon_counts)[:MAX_DISPLAY_OBSERVED])
self.user_taxa.update_observed(taxon_counts)
self.user_taxa.write()
self.on_load.emit(list(self.observed.cards))

@Slot(Taxon)
Expand Down
17 changes: 9 additions & 8 deletions naturtag/controllers/taxon_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from naturtag.app.style import fa_icon
from naturtag.constants import SIZE_SM
from naturtag.settings import UserTaxa
from naturtag.storage import AppState
from naturtag.widgets import (
GridLayout,
HorizontalLayout,
Expand Down Expand Up @@ -101,13 +101,14 @@ def __init__(self):

# View observations button
# TODO: Observation filters
self.view_observations_button = QPushButton('View Observations')
self.view_observations_button.setIcon(fa_icon('fa5s.binoculars', primary=True))
self.view_observations_button.clicked.connect(
lambda: self.on_view_observations.emit(self.displayed_taxon.id)
)
# self.view_observations_button = QPushButton('View Observations')
self.view_observations_button = QPushButton('')
# self.view_observations_button.setIcon(fa_icon('fa5s.binoculars', primary=True))
# self.view_observations_button.clicked.connect(
# lambda: self.on_view_observations.emit(self.displayed_taxon.id)
# )
self.view_observations_button.setEnabled(False)
self.select_button.setToolTip('View your observations of this taxon')
# self.select_button.setToolTip('View your observations of this taxon')
button_row_2.addWidget(self.view_observations_button)

# Link button: Open web browser to taxon info page
Expand Down Expand Up @@ -194,7 +195,7 @@ def _update_buttons(self):
class TaxonomySection(HorizontalLayout):
"""Section to display ancestors and children of selected taxon"""

def __init__(self, user_taxa: UserTaxa):
def __init__(self, user_taxa: AppState):
super().__init__()

self.ancestors_group = self.add_group(
Expand Down
3 changes: 1 addition & 2 deletions naturtag/metadata/inat_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@
from pyinaturalist import Observation, Taxon
from pyinaturalist_convert import to_dwc

from naturtag.client import iNatDbClient
from naturtag.constants import COMMON_NAME_IGNORE_TERMS, COMMON_RANKS, PathOrStr
from naturtag.metadata import MetaMetadata
from naturtag.settings import Settings
from naturtag.storage import Settings, iNatDbClient
from naturtag.utils import get_valid_image_paths, quote

DWC_NAMESPACES = ['dcterms', 'dwc']
Expand Down
Loading

0 comments on commit a10b563

Please sign in to comment.