Updates 0.23, trying to fix label updates
	
		
			
	
		
	
	
		
			
				
	
				CICD / Explore-Gitea-Actions (push) Successful in 35s
				
					Details
				
			
		
	
				
					
				
			
				
	
				CICD / Explore-Gitea-Actions (push) Successful in 35s
				
					Details
				
			
		
	This commit is contained in:
		
							parent
							
								
									c49ba8ff8d
								
							
						
					
					
						commit
						b78adf2366
					
				| 
						 | 
				
			
			@ -0,0 +1,191 @@
 | 
			
		|||
# Created by https://www.toptal.com/developers/gitignore/api/python,linux
 | 
			
		||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python,linux
 | 
			
		||||
 | 
			
		||||
### Linux ###
 | 
			
		||||
*~
 | 
			
		||||
 | 
			
		||||
# temporary files which can be created if a process still has a handle open of a deleted file
 | 
			
		||||
.fuse_hidden*
 | 
			
		||||
 | 
			
		||||
# KDE directory preferences
 | 
			
		||||
.directory
 | 
			
		||||
 | 
			
		||||
# Linux trash folder which might appear on any partition or disk
 | 
			
		||||
.Trash-*
 | 
			
		||||
 | 
			
		||||
# .nfs files are created when an open file is removed but is still being accessed
 | 
			
		||||
.nfs*
 | 
			
		||||
 | 
			
		||||
### Python ###
 | 
			
		||||
# Byte-compiled / optimized / DLL files
 | 
			
		||||
__pycache__/
 | 
			
		||||
*.py[cod]
 | 
			
		||||
*$py.class
 | 
			
		||||
 | 
			
		||||
# C extensions
 | 
			
		||||
*.so
 | 
			
		||||
 | 
			
		||||
# Distribution / packaging
 | 
			
		||||
.Python
 | 
			
		||||
build/
 | 
			
		||||
develop-eggs/
 | 
			
		||||
dist/
 | 
			
		||||
downloads/
 | 
			
		||||
eggs/
 | 
			
		||||
.eggs/
 | 
			
		||||
lib/
 | 
			
		||||
lib64/
 | 
			
		||||
parts/
 | 
			
		||||
sdist/
 | 
			
		||||
var/
 | 
			
		||||
wheels/
 | 
			
		||||
share/python-wheels/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
.installed.cfg
 | 
			
		||||
*.egg
 | 
			
		||||
MANIFEST
 | 
			
		||||
 | 
			
		||||
# PyInstaller
 | 
			
		||||
#  Usually these files are written by a python script from a template
 | 
			
		||||
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
			
		||||
*.manifest
 | 
			
		||||
*.spec
 | 
			
		||||
 | 
			
		||||
# Installer logs
 | 
			
		||||
pip-log.txt
 | 
			
		||||
pip-delete-this-directory.txt
 | 
			
		||||
 | 
			
		||||
# Unit test / coverage reports
 | 
			
		||||
htmlcov/
 | 
			
		||||
.tox/
 | 
			
		||||
.nox/
 | 
			
		||||
.coverage
 | 
			
		||||
.coverage.*
 | 
			
		||||
.cache
 | 
			
		||||
nosetests.xml
 | 
			
		||||
coverage.xml
 | 
			
		||||
*.cover
 | 
			
		||||
*.py,cover
 | 
			
		||||
.hypothesis/
 | 
			
		||||
.pytest_cache/
 | 
			
		||||
cover/
 | 
			
		||||
 | 
			
		||||
# Translations
 | 
			
		||||
*.mo
 | 
			
		||||
*.pot
 | 
			
		||||
 | 
			
		||||
# Django stuff:
 | 
			
		||||
*.log
 | 
			
		||||
local_settings.py
 | 
			
		||||
db.sqlite3
 | 
			
		||||
db.sqlite3-journal
 | 
			
		||||
 | 
			
		||||
# Flask stuff:
 | 
			
		||||
instance/
 | 
			
		||||
.webassets-cache
 | 
			
		||||
 | 
			
		||||
# Scrapy stuff:
 | 
			
		||||
.scrapy
 | 
			
		||||
 | 
			
		||||
# Sphinx documentation
 | 
			
		||||
docs/_build/
 | 
			
		||||
 | 
			
		||||
# PyBuilder
 | 
			
		||||
.pybuilder/
 | 
			
		||||
target/
 | 
			
		||||
 | 
			
		||||
# Jupyter Notebook
 | 
			
		||||
.ipynb_checkpoints
 | 
			
		||||
 | 
			
		||||
# IPython
 | 
			
		||||
profile_default/
 | 
			
		||||
ipython_config.py
 | 
			
		||||
 | 
			
		||||
# pyenv
 | 
			
		||||
#   For a library or package, you might want to ignore these files since the code is
 | 
			
		||||
#   intended to run in multiple environments; otherwise, check them in:
 | 
			
		||||
# .python-version
 | 
			
		||||
 | 
			
		||||
# pipenv
 | 
			
		||||
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
			
		||||
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
			
		||||
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
			
		||||
#   install all needed dependencies.
 | 
			
		||||
#Pipfile.lock
 | 
			
		||||
 | 
			
		||||
# poetry
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
 | 
			
		||||
#   This is especially recommended for binary packages to ensure reproducibility, and is more
 | 
			
		||||
#   commonly ignored for libraries.
 | 
			
		||||
#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
 | 
			
		||||
#poetry.lock
 | 
			
		||||
 | 
			
		||||
# pdm
 | 
			
		||||
#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
 | 
			
		||||
#pdm.lock
 | 
			
		||||
#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
 | 
			
		||||
#   in version control.
 | 
			
		||||
#   https://pdm.fming.dev/#use-with-ide
 | 
			
		||||
.pdm.toml
 | 
			
		||||
 | 
			
		||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 | 
			
		||||
__pypackages__/
 | 
			
		||||
 | 
			
		||||
# Celery stuff
 | 
			
		||||
celerybeat-schedule
 | 
			
		||||
celerybeat.pid
 | 
			
		||||
 | 
			
		||||
# SageMath parsed files
 | 
			
		||||
*.sage.py
 | 
			
		||||
 | 
			
		||||
# Environments
 | 
			
		||||
.env
 | 
			
		||||
.venv
 | 
			
		||||
env/
 | 
			
		||||
venv/
 | 
			
		||||
ENV/
 | 
			
		||||
env.bak/
 | 
			
		||||
venv.bak/
 | 
			
		||||
 | 
			
		||||
# Spyder project settings
 | 
			
		||||
.spyderproject
 | 
			
		||||
.spyproject
 | 
			
		||||
 | 
			
		||||
# Rope project settings
 | 
			
		||||
.ropeproject
 | 
			
		||||
 | 
			
		||||
# mkdocs documentation
 | 
			
		||||
/site
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
.dmypy.json
 | 
			
		||||
dmypy.json
 | 
			
		||||
 | 
			
		||||
# Pyre type checker
 | 
			
		||||
.pyre/
 | 
			
		||||
 | 
			
		||||
# pytype static type analyzer
 | 
			
		||||
.pytype/
 | 
			
		||||
 | 
			
		||||
# Cython debug symbols
 | 
			
		||||
cython_debug/
 | 
			
		||||
 | 
			
		||||
# PyCharm
 | 
			
		||||
#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
 | 
			
		||||
#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
 | 
			
		||||
#  and can be added to the global gitignore or merged into this file.  For a more nuclear
 | 
			
		||||
#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
 | 
			
		||||
#.idea/
 | 
			
		||||
 | 
			
		||||
### Python Patch ###
 | 
			
		||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
 | 
			
		||||
poetry.toml
 | 
			
		||||
 | 
			
		||||
# ruff
 | 
			
		||||
.ruff_cache/
 | 
			
		||||
 | 
			
		||||
# LSP config files
 | 
			
		||||
pyrightconfig.json
 | 
			
		||||
 | 
			
		||||
# End of https://www.toptal.com/developers/gitignore/api/python,linux
 | 
			
		||||
| 
						 | 
				
			
			@ -20,4 +20,4 @@ RUN pip install --no-cache-dir -r requirements.txt
 | 
			
		|||
COPY . .
 | 
			
		||||
 | 
			
		||||
CMD ["python", "main.py"]
 | 
			
		||||
#harbor.freshbrewed.science/library/vikunjamcp:0.22
 | 
			
		||||
#harbor.freshbrewed.science/library/vikunjamcp:0.23
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
  "name": "vikunja",
 | 
			
		||||
  "version": "1.0.22",
 | 
			
		||||
  "version": "1.0.23",
 | 
			
		||||
  "mcpServers": {
 | 
			
		||||
    "nodeServer": {
 | 
			
		||||
      "command": "docker",
 | 
			
		||||
| 
						 | 
				
			
			@ -14,7 +14,7 @@
 | 
			
		|||
         "VIKUNJA_USERNAME",
 | 
			
		||||
         "-e",
 | 
			
		||||
         "VIKUNJA_PASSWORD",
 | 
			
		||||
         "harbor.freshbrewed.science/library/vikunjamcp:0.22"
 | 
			
		||||
         "harbor.freshbrewed.science/library/vikunjamcp:0.23"
 | 
			
		||||
	  ],
 | 
			
		||||
      "env": {
 | 
			
		||||
         "VIKUNJA_URL": "$VIKUNJA_URL",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										99
									
								
								main.py
								
								
								
								
							
							
						
						
									
										99
									
								
								main.py
								
								
								
								
							| 
						 | 
				
			
			@ -560,7 +560,7 @@ def update_task_details(
 | 
			
		|||
 | 
			
		||||
    # at the least, id
 | 
			
		||||
    payload["id"] = task_id
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    # Simple helper to set only when value explicitly passed (not None)
 | 
			
		||||
    def set_if(provided, key, transform=lambda x: x):
 | 
			
		||||
        if provided is not None:
 | 
			
		||||
| 
						 | 
				
			
			@ -586,39 +586,35 @@ def update_task_details(
 | 
			
		|||
    # does not accept label updates; labels are managed via the labels bulk API.
 | 
			
		||||
    labels_payload = None
 | 
			
		||||
    if labels is not None:
 | 
			
		||||
        # normalize into array of strings (label titles) or ids
 | 
			
		||||
        # Vikunja expects an array of Label objects, not raw strings.
 | 
			
		||||
        # Normalize various inputs into a list of label objects like {"title": "..."} or {"id": 123}.
 | 
			
		||||
        def make_label_obj(item):
 | 
			
		||||
            # If it's already a dict, try to normalize known keys.
 | 
			
		||||
            if isinstance(item, dict):
 | 
			
		||||
                # If key 'name' used, convert to 'title' which Vikunja also accepts.
 | 
			
		||||
                if 'title' in item or 'id' in item:
 | 
			
		||||
                    return item
 | 
			
		||||
                if 'name' in item:
 | 
			
		||||
                    new = dict(item)
 | 
			
		||||
                    new['title'] = new.pop('name')
 | 
			
		||||
                    return new
 | 
			
		||||
                # Unknown dict shape - keep as-is but coerce to string title fallback.
 | 
			
		||||
                return {k: v for k, v in item.items()}
 | 
			
		||||
            # Integers are likely label IDs
 | 
			
		||||
            if isinstance(item, int):
 | 
			
		||||
                return {'id': item}
 | 
			
		||||
            # Otherwise treat as a title string
 | 
			
		||||
            return {'title': str(item)}
 | 
			
		||||
 | 
			
		||||
        if isinstance(labels, str):
 | 
			
		||||
            parsed = [s.strip() for s in labels.split(",") if s.strip()]
 | 
			
		||||
            labels_payload = {"labels": parsed}
 | 
			
		||||
            parsed = [s.strip() for s in labels.split(',') if s.strip()]
 | 
			
		||||
            labels_payload = {"labels": [make_label_obj(s) for s in parsed]}
 | 
			
		||||
        elif isinstance(labels, list):
 | 
			
		||||
            # If list contains dicts with id/title, pass through; otherwise coerce
 | 
			
		||||
            normalized = []
 | 
			
		||||
            for item in labels:
 | 
			
		||||
                if isinstance(item, dict):
 | 
			
		||||
                    # prefer 'title' or 'name' or 'id'
 | 
			
		||||
                    if 'title' in item:
 | 
			
		||||
                        normalized.append(item['title'])
 | 
			
		||||
                    elif 'name' in item:
 | 
			
		||||
                        normalized.append(item['name'])
 | 
			
		||||
                    elif 'id' in item:
 | 
			
		||||
                        normalized.append(item['id'])
 | 
			
		||||
                    else:
 | 
			
		||||
                        normalized.append(str(item))
 | 
			
		||||
                else:
 | 
			
		||||
                    normalized.append(item)
 | 
			
		||||
            labels_payload = {"labels": normalized}
 | 
			
		||||
            labels_payload = {"labels": [make_label_obj(i) for i in labels]}
 | 
			
		||||
        elif isinstance(labels, dict):
 | 
			
		||||
            # single dict -> try to extract title or id
 | 
			
		||||
            if 'title' in labels:
 | 
			
		||||
                labels_payload = {"labels": [labels['title']]}
 | 
			
		||||
            elif 'name' in labels:
 | 
			
		||||
                labels_payload = {"labels": [labels['name']]}
 | 
			
		||||
            elif 'id' in labels:
 | 
			
		||||
                labels_payload = {"labels": [labels['id']]}
 | 
			
		||||
            else:
 | 
			
		||||
                labels_payload = {"labels": [str(labels)]}
 | 
			
		||||
            labels_payload = {"labels": [make_label_obj(labels)]}
 | 
			
		||||
        else:
 | 
			
		||||
            labels_payload = {"labels": [str(labels)]}
 | 
			
		||||
            labels_payload = {"labels": [make_label_obj(labels)]}
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        logger.info("update_task_details: updating task_id=%s with payload keys=%s", task_id, list(payload.keys()))
 | 
			
		||||
| 
						 | 
				
			
			@ -626,11 +622,48 @@ def update_task_details(
 | 
			
		|||
        response.raise_for_status()
 | 
			
		||||
        task_update_resp = response.json()
 | 
			
		||||
 | 
			
		||||
        # If labels were provided, send them to the labels bulk endpoint
 | 
			
		||||
        # If labels were provided, resolve existing labels and send them to the labels bulk endpoint
 | 
			
		||||
        if labels_payload is not None:
 | 
			
		||||
            try:
 | 
			
		||||
                logger.info("update_task_details: updating labels for task_id=%s labels=%s", task_id, labels_payload)
 | 
			
		||||
                lab_resp = session.post(f"{VIKUNJA_URL}/api/v1/tasks/{task_id}/labels/bulk", json=labels_payload)
 | 
			
		||||
                logger.info("update_task_details: resolving existing labels from %s/labels", VIKUNJA_URL)
 | 
			
		||||
                # Fetch all existing labels for this user to try to reuse existing label IDs
 | 
			
		||||
                existing_resp = session.get(f"{VIKUNJA_URL}/api/v1/labels")
 | 
			
		||||
                existing_resp.raise_for_status()
 | 
			
		||||
                existing_labels = existing_resp.json() or []
 | 
			
		||||
 | 
			
		||||
                # Build a map of title(lowercase) -> id for quick lookup
 | 
			
		||||
                title_to_id = {}
 | 
			
		||||
                for lab in existing_labels:
 | 
			
		||||
                    if not isinstance(lab, dict):
 | 
			
		||||
                        continue
 | 
			
		||||
                    title = lab.get('title') or lab.get('name')
 | 
			
		||||
                    if title:
 | 
			
		||||
                        title_to_id[title.lower()] = lab.get('id')
 | 
			
		||||
 | 
			
		||||
                final_labels = []
 | 
			
		||||
                for item in labels_payload.get('labels', []):
 | 
			
		||||
                    # If already an id, pass through
 | 
			
		||||
                    if isinstance(item, dict) and 'id' in item:
 | 
			
		||||
                        final_labels.append({'id': item['id']})
 | 
			
		||||
                        continue
 | 
			
		||||
 | 
			
		||||
                    # Determine title string
 | 
			
		||||
                    if isinstance(item, dict):
 | 
			
		||||
                        title = item.get('title') or item.get('name') or str(item)
 | 
			
		||||
                    else:
 | 
			
		||||
                        title = str(item)
 | 
			
		||||
 | 
			
		||||
                    existing_id = title_to_id.get(title.lower())
 | 
			
		||||
                    if existing_id:
 | 
			
		||||
                        final_labels.append({'id': existing_id})
 | 
			
		||||
                    else:
 | 
			
		||||
                        # Create new labels via the bulk endpoint by providing title+description
 | 
			
		||||
                        final_labels.append({'title': title, 'description': title})
 | 
			
		||||
 | 
			
		||||
                final_payload = {'labels': final_labels}
 | 
			
		||||
 | 
			
		||||
                logger.info("update_task_details: updating labels for task_id=%s labels=%s", task_id, final_payload)
 | 
			
		||||
                lab_resp = session.post(f"{VIKUNJA_URL}/api/v1/tasks/{task_id}/labels/bulk", json=final_payload)
 | 
			
		||||
                lab_resp.raise_for_status()
 | 
			
		||||
                logger.info("update_task_details: labels updated successfully for task_id=%s", task_id)
 | 
			
		||||
            except requests.exceptions.RequestException as le:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue