mfiles-grpc 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mfiles_grpc-1.0.0/.github/workflows/ci.yml +52 -0
- mfiles_grpc-1.0.0/.github/workflows/publish.yml +91 -0
- mfiles_grpc-1.0.0/.github/workflows/tag.yml +71 -0
- mfiles_grpc-1.0.0/.gitignore +19 -0
- mfiles_grpc-1.0.0/LICENSE +21 -0
- mfiles_grpc-1.0.0/PKG-INFO +345 -0
- mfiles_grpc-1.0.0/README.md +310 -0
- mfiles_grpc-1.0.0/pyproject.toml +65 -0
- mfiles_grpc-1.0.0/scripts/generate_stubs.py +70 -0
- mfiles_grpc-1.0.0/scripts/live_object_test.py +168 -0
- mfiles_grpc-1.0.0/setup.cfg +4 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/__init__.py +8 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/__main__.py +115 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/_generated/__init__.py +1 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/_generated/mfilesCombinedWithDataPush_pb2.py +4878 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/_generated/mfilesCombinedWithDataPush_pb2_grpc.py +32611 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/client.py +266 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/config.py +119 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/objects.py +152 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/proto.py +19 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/sso.py +303 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/structure.py +57 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc/values.py +131 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/PKG-INFO +345 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/SOURCES.txt +34 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/dependency_links.txt +1 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/entry_points.txt +2 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/requires.txt +10 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/scm_file_list.json +30 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/scm_version.json +8 -0
- mfiles_grpc-1.0.0/src/mfiles_grpc.egg-info/top_level.txt +1 -0
- mfiles_grpc-1.0.0/tests/test_client.py +233 -0
- mfiles_grpc-1.0.0/tests/test_config.py +129 -0
- mfiles_grpc-1.0.0/tests/test_objects.py +93 -0
- mfiles_grpc-1.0.0/tests/test_sso.py +226 -0
- mfiles_grpc-1.0.0/tests/test_values.py +67 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v5
|
|
20
|
+
- uses: actions/setup-python@v6
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
- run: python -m pip install --upgrade pip
|
|
24
|
+
- run: pip install -e ".[dev]"
|
|
25
|
+
- run: flake8 --max-line-length 120 --extend-exclude src/mfiles_grpc/_generated src tests scripts
|
|
26
|
+
- run: pytest -q
|
|
27
|
+
|
|
28
|
+
build:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@v5
|
|
32
|
+
with:
|
|
33
|
+
fetch-depth: 0 # setuptools-scm needs the tags
|
|
34
|
+
- uses: actions/setup-python@v6
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.13"
|
|
37
|
+
- run: python -m pip install --upgrade build twine
|
|
38
|
+
- run: python -m build
|
|
39
|
+
- run: twine check --strict dist/*
|
|
40
|
+
# The package must never carry the .proto itself, only the stubs generated from it.
|
|
41
|
+
- name: No .proto in the package
|
|
42
|
+
run: |
|
|
43
|
+
for f in dist/*; do
|
|
44
|
+
if python -m zipfile -l "$f" 2>/dev/null | grep -q '\.proto$' || \
|
|
45
|
+
tar -tzf "$f" 2>/dev/null | grep -q '\.proto$'; then
|
|
46
|
+
echo "::error::$f contains a .proto file"; exit 1
|
|
47
|
+
fi
|
|
48
|
+
done
|
|
49
|
+
- uses: actions/upload-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Trusted Publishing: PyPI and TestPyPI trust this workflow, so no API token is stored.
|
|
4
|
+
# A GitHub release published on a v* tag → PyPI, and the files are attached to the release.
|
|
5
|
+
# Run by hand on a branch (Actions → Publish) → a .devN version on TestPyPI.
|
|
6
|
+
# The Tag workflow creates the v* tags on merges to main. The version comes from the tag
|
|
7
|
+
# (setuptools-scm).
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v5
|
|
22
|
+
with:
|
|
23
|
+
fetch-depth: 0 # setuptools-scm needs the tags
|
|
24
|
+
- uses: actions/setup-python@v6
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.13"
|
|
27
|
+
- run: python -m pip install --upgrade build twine
|
|
28
|
+
- run: python -m build
|
|
29
|
+
- name: Built version matches the tag
|
|
30
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
31
|
+
run: |
|
|
32
|
+
if [ ! -f "dist/mfiles_grpc-${GITHUB_REF_NAME#v}.tar.gz" ]; then
|
|
33
|
+
echo "::error::Tag $GITHUB_REF_NAME, but built $(ls dist)"; exit 1
|
|
34
|
+
fi
|
|
35
|
+
- run: twine check --strict dist/*
|
|
36
|
+
# The package must never carry the .proto itself, only the stubs generated from it.
|
|
37
|
+
- name: No .proto in the package
|
|
38
|
+
run: |
|
|
39
|
+
for f in dist/*; do
|
|
40
|
+
if python -m zipfile -l "$f" 2>/dev/null | grep -q '\.proto$' || \
|
|
41
|
+
tar -tzf "$f" 2>/dev/null | grep -q '\.proto$'; then
|
|
42
|
+
echo "::error::$f contains a .proto file"; exit 1
|
|
43
|
+
fi
|
|
44
|
+
done
|
|
45
|
+
- uses: actions/upload-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
testpypi:
|
|
51
|
+
if: github.event_name == 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/')
|
|
52
|
+
needs: build
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v5
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
62
|
+
with:
|
|
63
|
+
repository-url: https://test.pypi.org/legacy/
|
|
64
|
+
|
|
65
|
+
pypi:
|
|
66
|
+
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v')
|
|
67
|
+
needs: build
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
permissions:
|
|
70
|
+
id-token: write
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/download-artifact@v5
|
|
73
|
+
with:
|
|
74
|
+
name: dist
|
|
75
|
+
path: dist/
|
|
76
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
77
|
+
|
|
78
|
+
attach:
|
|
79
|
+
needs: pypi
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
permissions:
|
|
82
|
+
contents: write
|
|
83
|
+
steps:
|
|
84
|
+
- uses: actions/download-artifact@v5
|
|
85
|
+
with:
|
|
86
|
+
name: dist
|
|
87
|
+
path: dist/
|
|
88
|
+
- run: gh release upload "$GITHUB_REF_NAME" dist/*
|
|
89
|
+
env:
|
|
90
|
+
GH_TOKEN: ${{ github.token }}
|
|
91
|
+
GH_REPO: ${{ github.repository }}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: Tag
|
|
2
|
+
|
|
3
|
+
# When a pull request is merged to main, tag the merge commit with the next version. The pull
|
|
4
|
+
# request's labels choose the step:
|
|
5
|
+
# none → patch 1.2.3 → 1.2.4
|
|
6
|
+
# minor-version → minor 1.2.3 → 1.3.0
|
|
7
|
+
# major-version → major 1.2.3 → 2.0.0
|
|
8
|
+
# The first tag is v1.0.0.
|
|
9
|
+
#
|
|
10
|
+
# pull_request_target runs this file as it is on main, with a token that can write. It never
|
|
11
|
+
# checks out or runs the pull request's code; it reads only the labels and the merge commit.
|
|
12
|
+
# The "release tags" ruleset stops v* tags from being moved or deleted, but not created:
|
|
13
|
+
# GITHUB_TOKEN cannot be given a ruleset bypass.
|
|
14
|
+
#
|
|
15
|
+
# Tagging publishes nothing: creating a GitHub release on the tag starts Publish.
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
pull_request_target:
|
|
19
|
+
types: [closed]
|
|
20
|
+
branches: [main]
|
|
21
|
+
|
|
22
|
+
permissions: {}
|
|
23
|
+
|
|
24
|
+
concurrency:
|
|
25
|
+
group: tag
|
|
26
|
+
cancel-in-progress: false
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
tag:
|
|
30
|
+
if: github.event.pull_request.merged
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
permissions:
|
|
33
|
+
contents: write
|
|
34
|
+
env:
|
|
35
|
+
GH_TOKEN: ${{ github.token }}
|
|
36
|
+
GH_REPO: ${{ github.repository }}
|
|
37
|
+
SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
|
38
|
+
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
|
39
|
+
steps:
|
|
40
|
+
- name: Next version
|
|
41
|
+
id: next
|
|
42
|
+
run: |
|
|
43
|
+
gh api --paginate "repos/$GH_REPO/git/matching-refs/tags/v" -q '.[] | .ref + " " + .object.sha' |
|
|
44
|
+
python3 -c '
|
|
45
|
+
import json, os, re, sys
|
|
46
|
+
labels = set(json.loads(os.environ["LABELS"]))
|
|
47
|
+
versions = []
|
|
48
|
+
for line in sys.stdin:
|
|
49
|
+
ref, sha = line.split()
|
|
50
|
+
if m := re.fullmatch(r"refs/tags/v(\d+)\.(\d+)\.(\d+)", ref):
|
|
51
|
+
if sha == os.environ["SHA"]:
|
|
52
|
+
sys.exit(f"::error::{ref} already tags {sha}")
|
|
53
|
+
versions.append(tuple(map(int, m.groups())))
|
|
54
|
+
if not versions:
|
|
55
|
+
major, minor, patch = 1, 0, 0
|
|
56
|
+
else:
|
|
57
|
+
major, minor, patch = max(versions)
|
|
58
|
+
if "major-version" in labels:
|
|
59
|
+
major, minor, patch = major + 1, 0, 0
|
|
60
|
+
elif "minor-version" in labels:
|
|
61
|
+
minor, patch = minor + 1, 0
|
|
62
|
+
else:
|
|
63
|
+
patch += 1
|
|
64
|
+
print(f"tag=v{major}.{minor}.{patch}")
|
|
65
|
+
' >> "$GITHUB_OUTPUT"
|
|
66
|
+
- name: Create the tag
|
|
67
|
+
run: |
|
|
68
|
+
gh api "repos/$GH_REPO/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA" --silent
|
|
69
|
+
echo "Tagged $SHA as $TAG" >> "$GITHUB_STEP_SUMMARY"
|
|
70
|
+
env:
|
|
71
|
+
TAG: ${{ steps.next.outputs.tag }}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Working notes, not published
|
|
2
|
+
PLAN.md
|
|
3
|
+
|
|
4
|
+
# Build and test leftovers
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
__pycache__/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Never commit a real configuration: it holds the vault password
|
|
14
|
+
client-config.toml
|
|
15
|
+
proxide.log
|
|
16
|
+
|
|
17
|
+
# M-Files' protocol file is not published; only the stubs generated from it are.
|
|
18
|
+
# Keep a local copy here for scripts/generate_stubs.py.
|
|
19
|
+
*.proto
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jari Turkia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mfiles-grpc
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the M-Files gRPC API
|
|
5
|
+
Author-email: Jari Turkia <jatu@hqcodeshop.fi>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/M-Files/mfiles-grpc-python
|
|
8
|
+
Project-URL: Source, https://github.com/M-Files/mfiles-grpc-python
|
|
9
|
+
Project-URL: Issues, https://github.com/M-Files/mfiles-grpc-python/issues
|
|
10
|
+
Keywords: M-Files,gRPC,document management,ECM
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Office/Business
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: grpcio>=1.84.0
|
|
26
|
+
Requires-Dist: protobuf<8,>=7.35.1
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: grpcio-tools>=1.84.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
30
|
+
Requires-Dist: black>=26.3.1; extra == "dev"
|
|
31
|
+
Requires-Dist: flake8>=7.3.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build; extra == "dev"
|
|
33
|
+
Requires-Dist: twine; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# mfiles-grpc
|
|
37
|
+
|
|
38
|
+
Python client for the M-Files gRPC API: the protocol M-Files' own clients use.
|
|
39
|
+
It reaches parts of the vault the REST API (MFWS) does not, most usefully the
|
|
40
|
+
metadata structure. `POST /REST/structure/properties` answers HTTP 405, while
|
|
41
|
+
gRPC has `IRPCPropertyDefsAdmin.AddPropertyDef`, `AddObjectClass`,
|
|
42
|
+
`IRPCObjectTypesAdmin.AddObjectType` and a declarative whole-structure
|
|
43
|
+
get/set (`IRPCDeclarativeMetadataStructure`).
|
|
44
|
+
|
|
45
|
+
> **Not a supported public API.** The protocol comes from the M-Files Desktop
|
|
46
|
+
> client install and can change with any server update. Regenerate the stubs
|
|
47
|
+
> (see [Source of the .proto](https://github.com/M-Files/mfiles-grpc-python#source-of-the-proto)) after upgrading, and run the tests.
|
|
48
|
+
|
|
49
|
+
## Status
|
|
50
|
+
|
|
51
|
+
| What | State |
|
|
52
|
+
|---|---|
|
|
53
|
+
| gRPC on the REST host and port (443 on M-Files Cloud), path `/MFiles.<Service>/<Method>` | verified live |
|
|
54
|
+
| The `.proto` matches the server (live replies decode field for field) | verified live |
|
|
55
|
+
| Anonymous calls (`GetServerCapabilities`, `GetPublicKeyAnonymous`) | verified live |
|
|
56
|
+
| `LogIn` with user name and password → 48-byte session ID | verified live |
|
|
57
|
+
| Using that session on later calls | verified live (2026-09-24) |
|
|
58
|
+
| Connecting through a capturing proxy (`address`, `ca-cert`; Proxide) | verified live (2026-09-25) |
|
|
59
|
+
| SSO: reading the vault's OAuth settings (`auth-config`), anonymously | verified live (2026-09-24) |
|
|
60
|
+
| SSO: browser sign-in, `LogIn` with the token, session accepted | verified live (2026-09-24) |
|
|
61
|
+
| Structure helpers, reading object properties | verified live |
|
|
62
|
+
| Writing object properties (`set_properties`, with `expected_version` guard) | verified live (2026-09-24) |
|
|
63
|
+
| `create_object`, `remove_properties`, `delete_object`, `destroy_object` | verified live (2026-09-24) |
|
|
64
|
+
|
|
65
|
+
Verified against an M-Files Cloud vault. `scripts/live_object_test.py` repeats
|
|
66
|
+
the object checks against your vault: it creates a throwaway object, changes it,
|
|
67
|
+
adds values, checks that a write aimed at an old version is refused, empties one
|
|
68
|
+
value and removes another, then destroys the object. It **writes to the vault**;
|
|
69
|
+
on failure it prints the ID it left behind. `--keep` skips the delete. You name
|
|
70
|
+
an object type and properties of your own; see `--help`. For example:
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
python scripts/live_object_test.py --config client-config.toml --object-type eBook \
|
|
74
|
+
--integer "Page count" --optional-integer "Publishing year" --multiline Source
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
What it established:
|
|
78
|
+
|
|
79
|
+
* Creating an object needs `value_metadata` on every value (the helper adds
|
|
80
|
+
it); without it the server answers "Type mismatch."
|
|
81
|
+
* An object of a type that can have files cannot be created without
|
|
82
|
+
`Single file` (22).
|
|
83
|
+
* A property the class lists cannot be removed, only set empty (null).
|
|
84
|
+
`remove_properties` works only for properties the class does not list, such
|
|
85
|
+
as `Keywords` (26).
|
|
86
|
+
|
|
87
|
+
## How the session travels
|
|
88
|
+
|
|
89
|
+
`LogIn` returns a session ID, but none of the request messages has a field
|
|
90
|
+
for it: it travels in call metadata (HTTP/2 headers). The `.proto` does not
|
|
91
|
+
say so; the header names came from M-Files. The client adds these to every call:
|
|
92
|
+
|
|
93
|
+
| Header | Value |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `mfiles-session-id-bin` | `session_data.session_id` from `LogIn`, unchanged (after logging in) |
|
|
96
|
+
| `mfiles-is-remote-call` | `true` |
|
|
97
|
+
| `mfiles-activity-id-bin` | a new GUID for each call, for tracing |
|
|
98
|
+
|
|
99
|
+
gRPC base64-encodes `-bin` values itself. A call that needs a session and does
|
|
100
|
+
not carry `mfiles-session-id-bin` fails with UNAUTHENTICATED; a REST
|
|
101
|
+
`X-Authentication` token is not accepted in its place.
|
|
102
|
+
|
|
103
|
+
`mfiles-grpc check-session` logs in and makes one read-only call to confirm it.
|
|
104
|
+
|
|
105
|
+
## Logging in with SSO
|
|
106
|
+
|
|
107
|
+
Human users sign in through the vault's identity provider, not with an M-Files
|
|
108
|
+
password. The server does not run that sign-in for a client: the client gets a
|
|
109
|
+
token from the IdP itself and hands it to `LogIn`. The steps below mirror the
|
|
110
|
+
M-Files web client (26.10, `loginToServer` / `doPluginLogin` / `getConfig` in
|
|
111
|
+
`Common\Web\Public\mfapp-bundle-mfwebui.*.js`).
|
|
112
|
+
|
|
113
|
+
1. **Discovery, anonymous.** `IRPCLogin.GetAuthenticationConfiguration`, asked
|
|
114
|
+
with both `vault_guid` and `host_name`, once with `ACCOUNT_TYPE_MFILES` and then
|
|
115
|
+
with `ACCOUNT_TYPE_WINDOWS`. On M-Files Cloud only the Windows type answers.
|
|
116
|
+
The plugin with assembly `MFiles.AuthenticationProviders.OAuth` carries the IdP
|
|
117
|
+
settings as named values: `ClientID`, `AuthorizationEndpoint`,
|
|
118
|
+
`TokenEndpoint`, `Scope`, `RedirectURI`, `Resource`, `ClientSecret`,
|
|
119
|
+
`UseAccessTokenInWeb`. Its `system_configuration` gives the configuration
|
|
120
|
+
scope (`Scope`, for example `*:WINDOWS:`). `GetAuthenticationPlugins` does not
|
|
121
|
+
work here: anonymously it names the plugin but omits its configuration.
|
|
122
|
+
2. **Sign-in: authorization code with PKCE.** Like M-Files Desktop, the redirect is
|
|
123
|
+
`RedirectURI`, or `http://localhost` when there is none. On M-Files Cloud it is
|
|
124
|
+
`http://localhost/signin-oidc`. The IdP compares the redirect exactly, so the
|
|
125
|
+
one-shot listener binds its exact port, which is **80** when none is given. It
|
|
126
|
+
binds on both `127.0.0.1` and `::1`. The browser does the rest, including MFA.
|
|
127
|
+
3. **`LogIn` with `AUTH_DATA_TYPE_PLUGIN`**, in one round trip:
|
|
128
|
+
|
|
129
|
+
| Field | Value |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `plugin.plugin_name` | the plugin's `name` |
|
|
132
|
+
| `plugin.configuration_scope` | `system_configuration["Scope"]` |
|
|
133
|
+
| `plugin.authentication_attempt_identifier.data` | 32 zero bytes |
|
|
134
|
+
| `plugin.data_format` | `PLUGIN_AUTH_DATA_FORMAT_UNENCRYPTED` |
|
|
135
|
+
| `plugin.auth_data` | one named value, `Token`, as `DATATYPE_TEXT` |
|
|
136
|
+
|
|
137
|
+
The token is the **ID token**, or the access token when `UseAccessTokenInWeb`
|
|
138
|
+
is true. M-Files Cloud sets it to true. The session that comes back is the same kind as a password login,
|
|
139
|
+
so everything after `LogIn` is unchanged.
|
|
140
|
+
|
|
141
|
+
Configure it with:
|
|
142
|
+
|
|
143
|
+
```toml
|
|
144
|
+
[m-files.tool.common]
|
|
145
|
+
rest-api-url = "https://<vault>.cloudvault.m-files.com/REST/"
|
|
146
|
+
vault = "{GUID}" # no username or password
|
|
147
|
+
|
|
148
|
+
[m-files.tool.grpc]
|
|
149
|
+
auth = "sso"
|
|
150
|
+
# sso-token = "access" # if the server refuses the ID token
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`mfiles-grpc auth-config` shows what discovery found, anonymously. Check it
|
|
154
|
+
first. The redirect URI must be a loopback address. If the vault's IdP accepts
|
|
155
|
+
only the web client's `/signin-oidc`, a command line client cannot receive the
|
|
156
|
+
sign-in. In that case, put a token obtained elsewhere in the
|
|
157
|
+
`MFILES_GRPC_TOKEN` environment variable; it is used instead of the browser.
|
|
158
|
+
It goes in the environment rather than on the command line so that it does not
|
|
159
|
+
land in shell history or process listings.
|
|
160
|
+
|
|
161
|
+
On Linux, binding port 80 needs root or `CAP_NET_BIND_SERVICE`. Without either,
|
|
162
|
+
use `MFILES_GRPC_TOKEN`.
|
|
163
|
+
|
|
164
|
+
## Install
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
pip install mfiles-grpc
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Python 3.11 or newer. For working on the package itself: `pip install -e ".[dev]"`.
|
|
171
|
+
|
|
172
|
+
## Configure
|
|
173
|
+
|
|
174
|
+
Settings are read from a TOML file, `client-config.toml` by default
|
|
175
|
+
(`load_settings(path)`, `mfiles-grpc --config path`). The file holds the
|
|
176
|
+
password, so keep it out of version control and readable only by you
|
|
177
|
+
(`chmod 600`).
|
|
178
|
+
|
|
179
|
+
```toml
|
|
180
|
+
[m-files.tool.common]
|
|
181
|
+
rest-api-url = "https://<vault>.cloudvault.m-files.com/REST/"
|
|
182
|
+
vault = "{GUID}" # the vault's GUID, with braces
|
|
183
|
+
username = "..." # for auth = "password"
|
|
184
|
+
password = "..."
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The gRPC host and port are taken from `rest-api-url` (port 443 when the URL has
|
|
188
|
+
none). An optional `[m-files.tool.grpc]` section overrides them and chooses how
|
|
189
|
+
to log in:
|
|
190
|
+
|
|
191
|
+
```toml
|
|
192
|
+
[m-files.tool.grpc]
|
|
193
|
+
port = 443 # overrides the port in rest-api-url
|
|
194
|
+
address = "localhost:4443" # connect here instead, e.g. a capturing proxy
|
|
195
|
+
ca-cert = "proxide_ca.crt" # trust these root certificates (PEM) instead of the system's
|
|
196
|
+
auth = "sso" # "password" (default) or "sso"; see above
|
|
197
|
+
sso-token = "access" # optional: "id" or "access"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Capturing traffic through a proxy
|
|
201
|
+
|
|
202
|
+
To watch the calls in a man-in-the-middle proxy such as
|
|
203
|
+
[Proxide](https://github.com/Rantanen/proxide), leave `rest-api-url` at the real vault
|
|
204
|
+
and set `address` and `ca-cert`:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
proxide monitor -l 4443 -t <vault>.cloudvault.m-files.com:443
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```toml
|
|
211
|
+
[m-files.tool.grpc]
|
|
212
|
+
address = "localhost:4443"
|
|
213
|
+
ca-cert = "/path/to/proxide_ca.crt"
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
With `address` set only the connection goes there. The vault host is still the
|
|
217
|
+
name sent in the TLS handshake (SNI) and at login, and the name the certificate is
|
|
218
|
+
checked against. Proxide makes its certificate from that name and forwards it to
|
|
219
|
+
the vault. Do **not** point `rest-api-url` at the proxy instead: the handshake and
|
|
220
|
+
login would then name `localhost`, and the REST tools reading the same file would go
|
|
221
|
+
through the proxy too.
|
|
222
|
+
|
|
223
|
+
The capture contains the login request, **password included**; treat the proxy's
|
|
224
|
+
log as secret. With SSO it contains the token instead, which is just as secret.
|
|
225
|
+
|
|
226
|
+
## Use
|
|
227
|
+
|
|
228
|
+
The object type (101) and object (214) below are examples; use your vault's.
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from mfiles_grpc import Client, load_settings, objects, structure, values, pb
|
|
232
|
+
|
|
233
|
+
with Client.connect(load_settings()) as client:
|
|
234
|
+
source = structure.property_def_by_name(client, "Source", pb.DATATYPE_MULTI_LINE_TEXT)
|
|
235
|
+
print(objects.get_property_values(client, 101, 214))
|
|
236
|
+
objects.set_properties(client, 101, 214,
|
|
237
|
+
{source.id: values.multiline_text("…")},
|
|
238
|
+
expected_version=3)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Every service in the `.proto` is available as `client.stub("IRPC<Name>")`,
|
|
242
|
+
with the common ones as attributes: `client.objects`, `client.object_types`,
|
|
243
|
+
`client.property_defs`, `client.value_lists`, `client.search`,
|
|
244
|
+
`client.property_defs_admin`, `client.object_types_admin`. Messages and enum
|
|
245
|
+
values are on `pb` (`pb.SetPropertiesRequest`, `pb.DATATYPE_TEXT`).
|
|
246
|
+
|
|
247
|
+
### Command line
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
mfiles-grpc capabilities # anonymous; does the host speak gRPC?
|
|
251
|
+
mfiles-grpc auth-config # anonymous; the vault's SSO settings
|
|
252
|
+
mfiles-grpc login # are the credentials good?
|
|
253
|
+
mfiles-grpc check-session # is the session accepted?
|
|
254
|
+
mfiles-grpc structure # object types, classes, custom properties
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Safety built in
|
|
258
|
+
|
|
259
|
+
* `objects.set_properties()` always sends `remove_unspecified_properties=False`.
|
|
260
|
+
With `True`, `SetProperties` deletes every property not in the request;
|
|
261
|
+
across `SetPropertiesMultiple` that would wipe a library's metadata.
|
|
262
|
+
Use the raw stub if you really mean it.
|
|
263
|
+
* `expected_version=` makes a write fail rather than land on a version newer
|
|
264
|
+
than the one you read. Without it, the latest version is looked up first:
|
|
265
|
+
`GetProperties` answers "Not found" to the `LATEST` version marker, so the
|
|
266
|
+
helpers always send a real version number.
|
|
267
|
+
* `values.text()` refuses more than 100 characters. M-Files silently truncates
|
|
268
|
+
single-line text at 100; use `values.multiline_text()`.
|
|
269
|
+
* `values.normalise_newlines()`: M-Files stores multi-line text with CRLF, so
|
|
270
|
+
compare read-backs only after normalising.
|
|
271
|
+
* `ConnectionSettings` never prints the password or the SSO token.
|
|
272
|
+
|
|
273
|
+
Unchanged by the protocol: `Comment` (33) is per-version and not carried to new
|
|
274
|
+
versions; lookup names fold `ß` to `ss`; the Windows client still fails on
|
|
275
|
+
paths over 260 characters.
|
|
276
|
+
|
|
277
|
+
## Source of the .proto
|
|
278
|
+
|
|
279
|
+
The `.proto` file itself is not in this repository or the package; only the stubs
|
|
280
|
+
generated from it (`src/mfiles_grpc/_generated/`) are. It ships with every M-Files
|
|
281
|
+
Desktop client install, which is where to take it from:
|
|
282
|
+
|
|
283
|
+
| | |
|
|
284
|
+
|---|---|
|
|
285
|
+
| File | `mfilesCombinedWithDataPush.proto` |
|
|
286
|
+
| Taken from | M-Files Desktop client **26.9.16459.6**, `C:\Program Files\M-Files\26.9.16459.6\Common\Web\GRPC\proto\` |
|
|
287
|
+
| Date | 2026-09-23 |
|
|
288
|
+
| Size | 1053262 bytes |
|
|
289
|
+
| SHA-256 | `fcdfe3e144871941436b1869c28a25047d92db16771b2c7d3c31dbc7e3edaf0a` |
|
|
290
|
+
|
|
291
|
+
After a client upgrade, compare the new client's file against this hash:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
sha256sum mfilesCombinedWithDataPush.proto # Linux
|
|
295
|
+
Get-FileHash -Algorithm SHA256 mfilesCombinedWithDataPush.proto # PowerShell
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
If it differs, copy the new file into this directory (git ignores `*.proto`),
|
|
299
|
+
regenerate the stubs, run the tests, and update this table.
|
|
300
|
+
|
|
301
|
+
## Regenerating the stubs
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
python scripts/generate_stubs.py [path/to/mfilesCombinedWithDataPush.proto]
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Defaults to `mfilesCombinedWithDataPush.proto` in this directory. The script makes the
|
|
308
|
+
generated import package-relative and escapes the Windows paths in M-Files'
|
|
309
|
+
comments that would otherwise raise `SyntaxWarning` on import.
|
|
310
|
+
|
|
311
|
+
## Tests
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
pip install -e ".[dev]"
|
|
315
|
+
pytest
|
|
316
|
+
flake8 --max-line-length 120 --extend-exclude src/mfiles_grpc/_generated src tests scripts
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Offline; they need no vault. `scripts/live_object_test.py` is the live check.
|
|
320
|
+
|
|
321
|
+
## Releasing
|
|
322
|
+
|
|
323
|
+
Versions are never set by hand: every pull request merged to `main` gets the next one.
|
|
324
|
+
|
|
325
|
+
1. The Tag workflow tags the merge commit with the next version. The pull request's labels
|
|
326
|
+
choose the step:
|
|
327
|
+
|
|
328
|
+
| Label | Step | Example |
|
|
329
|
+
| --- | --- | --- |
|
|
330
|
+
| none | patch | 1.2.3 → 1.2.4 |
|
|
331
|
+
| `minor-version` | minor | 1.2.3 → 1.3.0 |
|
|
332
|
+
| `major-version` | major | 1.2.3 → 2.0.0 |
|
|
333
|
+
|
|
334
|
+
The first tag is `v1.0.0`.
|
|
335
|
+
2. To release, create a GitHub release on that tag (Releases → Draft a new release). Publishing
|
|
336
|
+
it starts the Publish workflow, which builds the package, publishes it to PyPI and attaches
|
|
337
|
+
the wheel and the sdist to the release. A tag without a release is not published.
|
|
338
|
+
|
|
339
|
+
The package version comes from the tag ([setuptools-scm](https://setuptools-scm.readthedocs.io/)),
|
|
340
|
+
so `pyproject.toml` holds none. Running Publish by hand on a branch publishes a `.devN` version
|
|
341
|
+
to TestPyPI.
|
|
342
|
+
|
|
343
|
+
## License
|
|
344
|
+
|
|
345
|
+
MIT; see [LICENSE](https://github.com/M-Files/mfiles-grpc-python/blob/main/LICENSE).
|