Skip to content

Conversation

@nicolasbisurgi
Copy link
Collaborator

Summary

Problem

The ApplicationService failed with 404 errors when accessing applications inside private folders because the TM1 REST API requires PrivateContents('FolderName') for private folder segments, but TM1py was always using Contents('FolderName') for intermediate path segments.

Solution: Implemented automatic path resolution with an optimistic probing strategy:

  1. Optimistic probing - Try all-public path first (most common case), then all-private path
  2. Fallback discovery - If both fail, iteratively probe each segment to find the exact public-to-private transition point
  3. Efficient probing - Uses $top=0 query parameter to probe paths without fetching actual data
  4. Optional caching - New use_cache parameter (default False) to cache discovered private boundaries for repeated access
  5. User warnings - Emits warnings when auto-resolution is used so users are aware of the path correction

Changes:

  • Added _build_path_url() helper to construct URL paths with mixed Contents/PrivateContents segments
  • Added _find_private_boundary() to iteratively discover where a path becomes private
  • Added _resolve_path() that implements the optimistic probing strategy
  • Updated all public methods (get_names, get, exists, delete, rename, create, update, etc.) to use path resolution
  • Added use_cache parameter to all affected methods
  • Added comprehensive unit tests for the new functionality

@nicolasbisurgi nicolasbisurgi self-assigned this Nov 28, 2025
@nicolasbisurgi nicolasbisurgi added the release:patch Triggers patch version bump (e.g.: 1.4.9 → 1.4.10) label Nov 28, 2025
@MariusWirtz
Copy link
Collaborator

The optimistic approach is very sensible. I like it. The costs for the iterations are acceptable IMO.

However, I noticed that the new code breaks some tests that run fine in the master branch

I think the rename function requires some additional handling, as the URLs with tm1.Move extension don't support GET.

image

…and discover()

This PR fixes issue #1317 where ApplicationService failed to access
applications inside private folders.

Key changes:
- Refactored _resolve_path() to properly handle mixed public/private
  folder hierarchies by probing path segments to find private boundaries
- Added _build_path_url() to construct correct URLs with PrivateContents
- Added _find_private_boundary() to detect where paths transition to private
- Added discover() method for exploring the Applications folder tree
- All application methods (get, get_document, exists, create, update,
  delete, rename) now correctly handle documents in private folders

The TM1 REST API requires different URL patterns:
- Public: /Contents('folder')/Contents('subfolder')
- Private: /PrivateContents('folder')/PrivateContents('subfolder')
- Mixed: /Contents('public')/PrivateContents('private')

Once inside a private folder, all nested content must use PrivateContents.
@nicolasbisurgi
Copy link
Collaborator Author

nicolasbisurgi commented Dec 17, 2025

Summary

New Features

discover() Method

Explore the Applications folder and discover all items including private assets:

# Discover all items at root level
items = tm1.applications.discover(path="", include_private=True)

# Discover recursively with nested structure
items = tm1.applications.discover(
    path="Planning Sample", 
    include_private=True, 
    recursive=True,
    flat=False  # Returns nested structure with 'children' key
)

# Each item contains: @odata.type, type, id, name, path, is_private

Copy Private Document to Public Location

from TM1py import TM1Service
from TM1py.Objects.Application import DocumentApplication

with TM1Service(**params) as tm1:
    # Get the private document
    private_doc = tm1.applications.get_document(
        path="Planning Sample/Administrator/PrivateFolder",
        name="My Private Document",
        private=True
    )
    
    # Create as public document
    public_doc = DocumentApplication(
        path="Planning Sample/Administrator",
        name="My Public Document",
        content=private_doc.content
    )
    
    tm1.applications.update_or_create(application=public_doc, private=False)

Access Documents in Nested Private Folders

# Now works correctly - previously failed with 404
doc = tm1.applications.get_document(
    path="Planning Sample/Administrator/PrivateFolder/SubFolder",
    name="Nested Document",
    private=True
)

Test Plan

  • All existing ApplicationService tests pass
  • Added tests for _build_path_url() with various boundary scenarios
  • Added tests for discover() (public, private, recursive, flat modes)
  • Added tests for document operations in private folders
  • Added tests for nested private folder structures
  • Added tests for copying documents between private/public locations

@github-actions
Copy link
Contributor

Tests completed for environment: tm1-11-cloud. Check artifacts for details.

@MariusWirtz
Copy link
Collaborator

Looks really good. Thank you, @nicolasbisurgi.

I hesitate a little bit with the private boundary cache mechanic.
TM1py so far is stateless. Doesn't this cache represent a minor change in approach?
What is the price for not caching? Is it a measurable loss in performance? Or too many micro requests that trigger PAoC remote disconnects?

No strong opinion, I just wanted to share my thoughts on this one.
@onefloid, what do you think?

@onefloid
Copy link
Collaborator

onefloid commented Jan 5, 2026

  1. Before we go further: is the trial-and-error approach via API calls the only way to determine whether a path is public or private, or is there any existing (possibly undocumented) API capability that we could leverage instead? @Hubert-Heijkers

  2. In my opinion, introducing a small cache here is a common and pragmatic optimization and does not fundamentally break statelessness.

  3. I would suggest changing the search strategy in order to reduce the number of API calls.

    Currently, boundary finding is implemented as a linear scan over the path segments. Since the privacy property along the path is monotonic (public → private → private …), we can instead apply a divide-and-conquer approach using a binary boundary search.

    This allows us to find the first private path segment in O(log n) API calls instead of O(n), while preserving the same functional behavior.

@nicolasbisurgi
Copy link
Collaborator Author

Thanks for the feedback!

Approach:

I do agree having @Hubert-Heijkers insight on this would be great. I was a bit surprised to see that there was no way to get this information without a trial an error approach.

Caching

Regarding caching, I think that if we leave it False as default we should still be considering TM1Py stateless. The reason I'm working on this topic is to create a script to transform all private applications into public ones for <500 user model, so having the ability to cache some of the valid mixed paths was a nice to have

Strategy

I agree, binary approach is more efficient but we would start seeing better performance starting on 4 level deep onwards, right? This might be more useful on v12 than v11 considering how Cloudfare handles repeated requests.

Next steps Proposal

I think we can proceed as is and wait for a response from Hubert; depending on where that answer leaves us we can optimize this approach by replacing linear by binary.

Thoughts?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:patch Triggers patch version bump (e.g.: 1.4.9 → 1.4.10)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ApplicationService fails to access applications inside private folders

4 participants